Linux Embedded

Le blog des technologies libres et embarquées

Implémentation du Secure Boot sur iMX8

Introduction au démarrage sécurisé

D’après le livre blanc de Smile sur la sécurité des objets connectés rédigé par Pierre Ficheux, l’internet des objets (IoT) a pris une place prépondérante dans l’industrie et dans la vie quotidienne. Après la généralisation des smartphones, les sources de données privées se diversifient et les problèmes de sécurité touchent des équipements informatiques très différents et se généralisent (véhicules, caméras, équipements médicaux, etc...). Si les systèmes d’exploitation des téléphones sont en général armés contre les attaques, il n’en est pas de même pour d’autres équipements pour lesquels la prise en compte des contraintes de sécurité laisse souvent à désirer.

Définition du démarrage sécurisé

Le secure boot, ou démarrage sécurisé, est un concept de sécurité ayant pour but d’assurer qu’un appareil démarre en utilisant uniquement des programmes approuvés par les développeurs et n’ayant pas été corrompus. 

Le cœur du démarrage sécurisé se situe dans l’assurance de l’authenticité et de l’intégrité des programmes lors du démarrage.

Les enjeux du démarrage sécurisé

Le démarrage sécurisé est principalement destiné à s’assurer que le produit est utilisé correctement et non de manière détournée. Le démarrage sécurisé permet également d’éviter l’exécution de programmes non approuvés, qui permettraient le retrait de certaines limitations logicielles par exemple. Il est aussi à destination de l’utilisateur final afin qu'il soit assuré que le système n’a pas été modifié avec, par exemple, l’ajout d’une backdoor.

En règle générale, le démarrage sécurisé permet d'avoir l’assurance que tous les binaires chargés, démarrés et exécutés ont été générés par une personne de confiance.

Comment ça marche le démarrage sécurisé?

Le démarrage sécurisé est basé sur de la vérification de signature, qui n’est pas à confondre avec le chiffrement. Dans la pratique, le premier élément du processus de démarrage va vérifier le second qui va vérifier le troisième, etc… Cette chaîne de vérification est appelée la chain-of-trust. Nous allons voir comment authentifier chaque élément de cette chain-of-trust pour avoir un démarrage sécurisé fonctionnel.

Chain-of-trust

Root-of-trust

La famille de processeurs iMX8 intègre des modules de sécurité avancés fournissant des fonctionnalités nécessaires au démarrage sécurisé. 

Le module CAAM (Cryptographic Acceleration and Assurance Module), permet l’accélération matérielle des algorithmes cryptographiques. 

Le module OCOTP_CTRL (On-Chip OTP Controller) permet à l’utilisateur d’interagir avec les OTP (One Time Programmable) : fusibles programmables du SoC (System On Chip). Ceux-ci permettent de configurer les modes de démarrage et de stocker des empreintes de clés publiques. Stocker directement des clés est assez coûteux en ressources, il est donc plus économique de stocker uniquement les empreintes de ces clés.

Enfin, le module HAB (High Assurance Boot : une fonctionnalité du boot ROM) permet l’ajout d’une étape de vérification d’authenticité des images avant de les exécuter. Pour cela, le HAB utilise les clés publiques stockées dans les OTP et le CAAM pour accélérer le processus de vérification.

Ces trois éléments représentent les modules principaux du SoC entrant en jeu dans l’implémentation du démarrage sécurisé. Leurs interactions sont résumées sur le schéma ci-dessous. 

root of trust

L’élément important à retenir est l’impossibilité de modifier le code contenu de la ROM qui contient le code HAB. Comme ce code ne peut pas être modifié, il constitue le root-of-trust.

PKI tree

Afin de générer des clés compatibles avec le HAB il est nécessaire d’utiliser l’outil CST (Code Signing Tool). Il s’agit d’un outil développé par NXP permettant de générer des clés et des certificats ou bien de signer des images. La structure de l’organisation des clés gérée par HAB est sous la forme d’un arbre : un PKI tree, comme nous pouvons le voir sur la figure ci-dessous.

CST
  • Le CA (Autorité de certification) est utilisé uniquement pour signer les SRK (Super Root Key)
  • Les SRK sont utilisées pour signer les certificats CSF (Command Sequence File) et IMG (image).

Un CSF est un fichier contenant des informations nécessaires au HAB pour vérifier l’authenticité d’une image : ce fichier va notamment contenir la taille de l’image, les adresses des blocs authentifiés, les algorithmes utilisés ou encore la taille de la clé utilisée. Un fichier IMG contient le binaire suivant dans notre chaine de boot : le bootloader (u-boot).

Notre HAB va donc utiliser le SRK pour vérifier la signature du CSF, puis utiliser le SRK et les informations contenues dans le CSF pour vérifier l'IMG avant d'exécuter celle-ci.

Il peut y avoir jusqu’à 4 SRK, dont chacune est utilisée pour signer un certificat CSF et un certificat IMG. La présence de plusieurs SRK donne la possibilité de révoquer une clé. Par exemple, si la SRK 1 a été divulguée, il est possible de l'invalider puis d’utiliser la SRK suivante, évitant ainsi de briquer la carte après une fuite de clé. La table d’empreintes des SRK est programmée dans l’OTP. Seulement une des SRK de la table est utilisée lors du démarrage et la sélection se fait grâce à un paramètre présent dans un CSF.

  • Le certificat CSF permet la validation de la partie CSF d’une image à flasher. La "partie CSF d’une image" est la version binaire du fichier CSF qui se situe à la suite de l’image.
  • Le certificat IMG est utilisé pour valider la portion du binaire contenant l’image en elle-même.

Génération des clés et programmation sur la carte

Dans un premier temps, il faut installer l’outil CST, pour cela il faut se rendre sur le site de NXP et créer un compte puis décompresser l’archive et se rendre dans le dossier précédemment décompressé : 

~$ tar xzf cst-3.3.1.tar.gz
~$ cd cst-3.3.1/keys

Ensuite, il faut créer deux fichiers dans le répertoire keys. Un fichier serial qui va contenir le numéro de série et un fichier key_pass.txt qui va contenir le mot de passe protégeant les clés privées. Ces fichiers sont utilisés par OpenSSL lors de la génération des clés.

~/cst.3.3.1/keys$ echo "42424242" > serial
 
~/cst.3.3.1/keys$ cat << EOF > key_pass.txt
linuxembedded123!
linuxembedded123!
EOF

On peut à présent générer les clés en utilisant le script hab4_pki_tree.sh se trouvant dans le même répertoire.

Ce script propose une configuration interactive, permettant ainsi de choisir la taille des clés, le nombre de SRK à générer, la durée de validité des clés, et le type d’algorithme.

~/cst.3.3.1/keys$ ./hab4_pki_tree.sh                                               
...

Do you want to use an existing CA key (y/n)?: n
Do you want to use Elliptic Curve Cryptography (y/n)?: n
Enter key length in bits for PKI tree: 2048
Enter PKI tree duration (years): 3
How many Super Root Keys should be generated? 4

...

Après avoir lancé ce script, les clés sont générées dans le répertoire /keys et les certificats correspondants dans le répertoire /crts.

Il est possible d’utiliser des clés de 1024, 2048, 3072 ou 4096 bits. Le choix de cette taille peut être éclairé en utilisant le site keylength.com. Ce site évalue la taille de clé à choisir en fonction de la limite en année pour laquelle le système doit être protégé. Cela permet d’avoir un compromis entre la vitesse de calcul et la durée de vie du système.

Maintenant que les clés sont générées, la prochaine étape consiste à générer la table d’empreintes des clés afin de les stocker dans les OTP.

Pour générer la table d’empreintes, il faut utiliser l’outil srktool présent dans le CST :

~/cst.3.3.1/keys$ cd ../crts/


~/cst.3.3.1/crts$ ../linux64/bin/srktool  \  
-h 4                                      \
-t SRK_1_2_3_4_table.bin                  \
-e SRK_1_2_3_4_fuse.bin                   \
-d sha256                                 \
-c ./SRK1_sha256_2048_65537_v3_ca_crt.pem,\  
   ./SRK2_sha256_2048_65537_v3_ca_crt.pem,\
   ./SRK3_sha256_2048_65537_v3_ca_crt.pem,\ 
   ./SRK4_sha256_2048_65537_v3_ca_crt.pem\  
-f 1                                  

-h : HAB Version
-t : Filename for output SRK table binary file
-e : Filename for the output SRK efuse binary file containing the SRK table hash
-d : Message Digest algorithm
-c : X.509v3 certificate filenames.    
-f : Optional, Data format of the SRK efuse binary file.

Ensuite, nous affichons les binaires à programmer dans les  OTP :

~/cst.3.3.1/crts$ hexdump -e '/4 "0x"' -e '/4 "%08X""\n"' < SRK_1_2_3_4_fuse.bin
0x03402271
0x67166C19
0x64679AE8
0x25056CEE
0x0676D664
0xE46DD5CA
0x3A561C27
0x9E6742BA                    

Les manipulations suivantes sont à réaliser dans U-Boot, en faisant très attention à bien recopier les valeurs ci-dessus, car une fois programmées, vous ne pourrez plus jamais les changer.

Assurez-vous d’utiliser vos propres valeurs en utilisant les commandes suivantes, autrement votre carte ne pourra jamais authentifier l’image du bootloader.

=> fuse prog -y 6 0 0x3402271
=> fuse prog -y 6 1 0x67166C19
=> fuse prog -y 6 2 0x64679AE8
=> fuse prog -y 6 3 0x25056CEE
=> fuse prog -y 7 0 0x676D664
=> fuse prog -y 7 1 0xE46DD5CA
=> fuse prog -y 7 2 0x3A561C27
=> fuse prog -y 7 3 0x9E6742BA                   

Nous venons de coder en dur et de façon définitive les éléments qui permettront à notre HAB d'authentifier le bootloader. Il faut maintenant générer une image de bootloader signée avec ces mêmes clés.

Bootloader

Pour nos manipulations, nous utilisons la carte Boundary Device Nitrogen8M Mini. La version de U-Boot issue de Boundary Device n’est pas configurée pour supporter les fonctionnalités HAB, or, nous avons besoin que U-Boot soit capable d'utiliser HAB pour vérifier l'image du noyau Linux que nous souhaitons démarrer.

Il est donc nécessaire d’activer cette fonctionnalité lors de la compilation de U-Boot.

Pour cela, il faut, dans un premier temps, cloner la branche des sources de compilation de U-Boot pour les cartes iMX8, ensuite configurer les fonctionnalités HAB dans U-Boot avant de compiler.

~$ git clone https://github.com/boundarydevices/u-boot-imx6 -b boundary-v2018.07

~$ cd u-boot-imx6
~/u-boot-imx6$ export ARCH=arm64
~/u-boot-imx6$ export CROSS_COMPILE=aarch64-linux-gnu-
~/u-boot-imx6$ make nitrogen8mm_2g_defconfig
~/u-boot-imx6$ make menuconfig

Pour la configuration de U-Boot nous allons donc partir d’une configuration de base qui se nomme nitrogen8mm_2g_defconfig, puis y ajouter nos configurations.

Dans le menuconfig nous allons activer les fonctions HAB avec le chemin suivant : 

ARM architecture → Support i. MX HAB features

Une fois que cette option est activée, il ne reste plus qu’à compiler l’image :

~/u-boot-imx6$ make flash.bin

À l’issue de la compilation, nous obtenons le fichier flash.bin contenant l’image U-Boot avec les fonctionnalités HAB activée.

a) Signature

Pour signer l’image, nous utilisons le script sign_hab_imx8m.sh présent dans les sources. Ce script nécessite d’importer les variables suivantes :

~/u-boot-imx6$ export CST_BIN=~/cst-3.3.1/linux64/bin/cst
~/u-boot-imx6$ export SIGN_KEY=~/cst-3.3.1/crts/CSF_crt.pem
~/u-boot-imx6$ export IMG_KEY=~/cst-3.3.1/crts/IMG_crt.pem
~/u-boot-imx6$ export SRK_TABLE=~/cst-3.3.1/crts/SRK_table.bin

Les variables sont des chemins vers les certificats de signatures et de la table SRK générée précédemment ainsi que les outils de compilation des CSF. Nous pouvons maintenant exécuter le script :

~/u-boot-imx6$ ./sign_hab_imx8m.sh

CSF Processed successfully and signed data available in csf_spl.bin
CSF Processed successfully and signed data available in csf_fit.bin

signed_flash.bin is ready!

Pour résumer, ce script va utiliser les logs de compilation de flash.bin pour obtenir les adresses et les offsets des éléments du bootloader qui sont : le SPL et U-Boot. Puis il va remplir deux templates de fichiers CSF pour chaque élément avec les bonnes clés et adresses. Enfin, le script va compiler les fichiers CSF à l’aide de CST_BIN avant de les concaténer aux SPL et U-Boot puis tout concaténer ensemble pour obtenir l’image finale :  signed-flash.bin

Afin de mieux comprendre le fonctionnement de la signature, nous allons maintenant voir le contenu de signed-flash.bin

signed-flash.bin

Comme nous pouvons le voir sur la figure ci-dessus, le fichier signed-flash.bin contient deux sous-images distinctes : SPL Image et FIT Image.

L’image SPL (pour Second Phase Loader) est composée d’un firmware permettant d’initialiser les horloges et la mémoire. L’IVT (Image Vector Table), correspond à une table d’adresses des différents éléments composant l’image. 

Le HAB va tester l'authenticité des données signées grâce au CSF SPL, avant de démarrer le SPL 

L’image FIT, pour Flattened Image Tree, contient le binaire de U-Boot et son DTB (Device Tree Blob). 

Le SPL va authentifier l'image FIT grâce aux informations du CSF FIT en utilisant l’API HAB. 

b) Flashage

Il existe plusieurs méthodes pour flasher une image sur la carte, mais la plus simple est d’utiliser la commande suivante dans U-Boot : 

=> ums mmc 0

UMS (USB Mass Storage) permet d’avoir accès directement à la mémoire eMMC de la carte grâce à un câble USB reliant la carte et le PC hôte. La mémoire est alors vue comme une clé USB par le système hôte.

Les étapes exactes de flashage sont détaillées ici : https://boundarydevices.com/u-boot-v2016-03/

Une fois l’image chargée, la commande suivante va mettre à jour et redémarrer U-Boot.

=> run upgradeu 

Pour vérifier que les fonctionnalités HAB sont présentes et que la signature a bien été vérifiée, nous devons avoir le résultat suivant lors de l’exécution de la commande hab_status :

=> hab_status

Secure boot disabled

HAB Configuration: 0xf0, HAB State: 0x66
No HAB Events Found!

La ligne indiquant « Secure boot disabled » ne signifie pas vraiment que le démarrage sécurisé n'est pas fonctionnel, mais que la carte n’est pas en mode sécurisé. En effet, il existe une commande permettant de passer la carte en mode sécurisé de manière définitive. Dans ce mode sécurisé, la carte ne pourra pas démarrer d’images non authentifiées avec les clés précédemment programmées sur la carte. 

En mode non sécurisé, la carte va seulement notifier des évènements détectés par HAB. Un évènement désigne la présence d’une image non sécurisée : une image corrompue, non signée, ou signée avec une mauvaise clé, etc…

« No HAB Events Found! » signifie donc que l’image est bien authentifiée avec la bonne clé.

Pour passer en mode sécurisé, on utilise la commande suivante : 

Cette étape est irréversible, assurez vous qu'il n'y ai pas de HAB Events en mode non sécurisé. 

=> fuse prog 1 3 0x02000000
=> reset
...
=> hab_status 

Secure boot enabled

HAB Configuration: 0xcc, HAB State: 0x99
No HAB Events Found!

c) Test de corruption

Maintenant que nous avons authentifié le bootloader, il est intéressant de tester si notre système est vraiment capable de détecter des images non authentifiées.

Pour cela, nous allons générer plusieurs images U-Boot avec chacune des spécificités différentes.

Une première image où nous avons modifié 1 seul bit de l’image de U-Boot, une seconde image sans signature, mais avec les fonctions HAB activées et enfin une image signée, mais avec des clés différentes de celles programmées dans l’OTP.

On a obtenu le résultat suivant pour l’image corrompue :

=> hab_status

HAB Configuration: 0xf0, HAB State: 0x66

--------- HAB Event 1 -----------------
event data:
0xdb 0x00 0x1c 0x43 0x33 0x18 0xc0 0x00
0xca 0x00 0x14 0x00 0x02 0xc5 0x1d 0x00
0x00 0x00 0x0d 0x44 0x00 0x7e 0x0f 0xc0
0x00 0x03 0x7a 0x00

STS = HAB_FAILURE (0x33)
RSN = HAB_INV_SIGNATURE (0x18)
CTX = HAB_CTX_COMMAND (0xC0)
ENG = HAB_ENG_ANY (0x00)
...

On observe maintenant des HAB Events et ce qui va nous intéresser est la variable RSN qui signifie Reason. Nous pouvons observer que HAB a détecté une mauvaise signature, ce qui signifie que l’empreinte de l’image ne correspond pas à l’empreinte de la signature.

Pour une image non corrompue mais sans signature nous obtenons l’évènement suivant :

=> hab_status

HAB Configuration: 0xf0, HAB State: 0x66

--------- HAB Event 1 -----------------
event data:
0xdb 0x00 0x08 0x43 0x33 0x11 0xcf 0x00

STS = HAB_FAILURE (0x33)
RSN = HAB_INV_CSF (0x11)
CTX = HAB_CTX_CSF (0xCF)
ENG = HAB_ENG_ANY (0x00)
...

Cet évènement signifie que le fichier CSF est invalide ce qui est logique, car il n’y en a pas.

Enfin, nous avons testé une image non corrompue, mais signée avec des clés différentes de celle programmées en dur sur la carte :

=> hab_status

HAB Configuration: 0xf0, HAB State: 0x66

--------- HAB Event 1 -----------------
event data:
0xdb 0x00 0x14 0x43 0x33 0x21 0xc0 0x00
0xbe 0x00 0x0c 0x00 0x03 0x17 0x00 0x00
0x00 0x00 0x00 0x68

STS = HAB_FAILURE (0x33)
RSN = HAB_INV_CERTIFICATE (0x21)
CTX = HAB_CTX_COMMAND (0xC0)
ENG = HAB_ENG_ANY (0x00)

...

Il est indiqué que le certificat n’est pas valide, ce qui est cohérent.

Nous avons donc validé l’authentification du bootloader dans notre chain-of-trust et il est maintenant temps d’authentifier le prochain élément de notre chain-of-trust : le kernel. 

Kernel

a) Signature

Comme pour U-Boot, il existe un script permettant de signer le kernel :

~/u-boot-imx6$ ./sign_hab_imx8m-Image.sh ./Image

À l’issue de cette commande, qui prend en paramètre l’image du kernel, nous obtenons en sortie l’image signée : Image-Signed.bin ainsi qu’un offset à conserver pour la vérification de l’authenticité de l’image dans U-Boot.

b) Test d'authentification et test de corruption

Pour tester l’authenticité du kernel, il existe la fonction hab_auth_img qui va vérifier la signature du kernel. Cette fonction prend en paramètre l’adresse où le kernel a été chargé, la taille du kernel et l’offset obtenu lors de la signature. Nous avons utilisé la commande tftp pour charger le kernel en mémoire.

=> tftp $loadaddr $serverip:Image-signed.bin


=> hab_auth_img ${loadaddr} ${filesize} 0x01c46000
hab fuse not enabled

Authenticate image from DDR location 0x40480000...

Secure boot disabled

HAB Configuration: 0xf0, HAB State: 0x66
No HAB Events Found!

Pour vérifier que l’authentification marche correctement nous avons essayé de faire les mêmes manipulations qu’avec le bootloader, en chargeant une image corrompue. Nous avons donc modifié un bit dans l’image du kernel et obtenu la sortie suivante :

=> tftp $loadaddr $serverip:Image-signed.bin                                         


=> hab_auth_img ${loadaddr} ${filesize} 0x01c46000
hab fuse not enabled

Authenticate image from DDR location 0x40480000...

Secure boot disabled

HAB Configuration: 0xf0, HAB State: 0x66

--------- HAB Event 1 -----------------
event data:
	0xdb 0x00 0x1c 0x43 0x33 0x18 0xc0 0x00
	0xca 0x00 0x14 0x00 0x02 0xc5 0x1d 0x00
	0x00 0x00 0x0d 0x3c 0x40 0x48 0x00 0x00
	0x01 0xc4 0x60 0x20

STS = HAB_FAILURE (0x33)
RSN = HAB_INV_SIGNATURE (0x18)
CTX = HAB_CTX_COMMAND (0xC0)
ENG = HAB_ENG_ANY (0x00)

La corruption de l’image a donc été bien détectée comme une signature invalide.

Rootfs

Dans cette dernière partie, nous allons voir la dernière étape de notre chain-of-trust qui est l’authentification du Rootfs.

La vérification de l'authenticité du rootfs ne va pas se faire comme dans les deux parties précédentes, en effet il serait beaucoup trop chronophage de calculer l’empreinte de tout le rootfs afin d'en vérifier l'authenticité. À titre de comparaison, notre image U-Boot pèse environ 2 Mo, celle du kernel 30 Mo et le rootfs 160 Mo. Nous allons utiliser un mécanisme plus adapté : device mapper et, plus précisément : dm-verity.

a) Implémentation de DM-Verity

Pour assurer l’intégrité d’un block device, dm-verity va utiliser ce qu’on appelle un arbre de Merkle. Il s'agit d'un arbre de hash incluant l’empreinte de tous les blocs physiques. Les feuilles de l’arbre correspondent aux empreintes des données du block device et les nœuds intermédiaires aux empreintes des nœuds enfants, c’est-à-dire des hashs de hashs. Le nœud au sommet est le root hash. Voici ci-dessous un exemple d’arbre de Merkle :

Merkle tree

Cet arbre de Merkle va être précalculé lors de la construction de l’image puis stocké à côté des données à vérifier. Dans les parties précédentes, afin d’assurer l’intégrité des données de l’étape N+1 de la chain-of-trust, nous stockions l’empreinte de l’image N+1 sous la forme d’une signature. Il suffisait ensuite de recalculer l’empreinte de l’image et le comparer avec l’empreinte de la signature (et de valider la signature de l'empreinte avec la clé publique).

Cependant, recalculer l’empreinte de tout un rootfs prendrait beaucoup trop de temps, c’est pourquoi dans cette partie nous allons conserver uniquement le root hash. La moindre modification dans les data blocks entraînerait un root hash totalement différent. Néanmoins, pour que ce root hash soit effectif, il faut qu’il soit stocké dans une partie de confiance de la chain-of-trust. Dans notre cas, le root hash est stocké dans les paramètres d’environnements de U-Boot, donc dans une zone de confiance à terme.

L’arbre de Merkle est utilisé par dm-verity pour vérifier un bloc sans pour autant avoir à recalculer l’arbre en entier. Par exemple, si nous voulons vérifier l’intégrité du bloc BD de la figure ci-dessus, nous calculons son empreinte, HD et nous avons uniquement besoin de charger les empreintes HC HAB HEFGH pour recalculer le root-hash et le comparer à celui enregistré dans l'environnement u-boot.

Nous avons vu précédemment que dm-verity assurait l'intégrité des données, mais qu’en est-il de l’authenticité ? L’authenticité provient du fait que le root hash soit stocké dans une partie de confiance N-1 de la chain-of-trust, empêchant ainsi la possibilité de charger un rootfs intègre quelconque. 

b) Configuration dm-verity

Voici les configurations nécessaires dans Yocto pour utiliser dm-verity :

Le rootfs doit être en lecture seule (c'est imposé par dm-verity) :  

IMAGE_FEATURES_append= “ read-only-rootfs”

dm-verity a besoin des packages suivants pour fonctionner : 

IMAGE_INSTALL_append= “ lvm2 cryptsetup”

Enfin, il faut activer dm-verity dans le kernel, pour cela dans le menuconfig du kernel, il faut activer : Verity target support et DM ”dm-mod.create=” parameter support.

c) Implémentation DM-Verity

Nous allons maintenant voir comment implémenter concrètement dm-verity, pour cela nous allons dans un premier temps créer un dossier à part, contenant uniquement le rootfs afin de pouvoir y faire les manipulations nécessaires.

~$ mkdir rootfs & cd rootfs
~/rootfs$ cp ~/gatesgarth/build/tmp/deploy/images/nitrogen8mm/*.rootfs.ext4 .

Pour faciliter la configuration de dm-verity nous allons utiliser l’outil veritysetup. Dans un premier temps, nous allons donc générer l’arbre de Merkle avec la commande suivante :

~/rootfs$ sudo veritysetup format ./*.rootfs.ext4 rootfs.ext4.hash\
          --data-block-size=1024 

On peut observer qu’on a spécifié la taille des data blocks à 1024, car la valeur par défaut, 4096, produisait une erreur lors du démarrage du kernel. Cela est dû au fait que lors de la compilation du rootfs dans Yocto, l’alignement par défaut est de 1024.

À l’issue de cette commande, l’arbre de Merkle va être stocké dans le fichier rootfs.ext4.hash. Cette commande va également afficher d’autres informations utiles, comme le root hash par exemple :

VERITY header information for rootfs.ext4.hash
UUID:            	06c896df-5994-4f98-ad2b-dfc282b67a99
Hash type:       	1
Data blocks:     	157009
Data block size: 	1024
Hash block size: 	4096
Hash algorithm:  	sha256
Salt:      bc00e89071c1ef9cc0ba337bb4c0ce240af15ba7070fa30cbf611af7f8bda325
Root hash: b9084c496613f66036108a12cfa0b3dc64ff32c46ad9d3d960280ace0c57fb11

On peut maintenant copier le rootfs et l’arbre de Merkle dans les bonnes partitions de la carte. En effet, dans notre cas, le rootfs et l’arbre de Merkle ne sont pas stockés dans la même partition. Nous avons donc créé une 3ème partition à coté de la partition de boot et la partition du rootfs, qui contient l’arbre de Merkle. 

Pour créer la 3ème partition, nous avons utilisé la commande « ums mmc 0 » dans U-Boot, puis l’outil de partitionnement graphique GParted sur le PC hôte.

On flash ensuite nos images :

~/rootfs$ dd if=./rootfs.ext4.hash  of=/dev/sdX3 
~/rootfs$ dd if=./*.rootfs.ext4       of=/dev/sdX2 

La commande ci-dessous va permettre d’activer le device mapper dm-verity, nommé vroot, en faisant le lien entre le rootfs, l’arbre de Merkle et le root hash. Nous lançons cette commande sur le PC hôte dans le but de tester qu’il n’y a pas d’erreur lors de la génération des fichiers.

~/rootfs$ sudo veritysetup open /dev/sdd2 vroot /dev/sdd3 <root hash> --debug

On va ensuite modifier l’environnement U-Boot sur la cible, et ajouter aux paramètres du kernel l’option « dm-mod.create= » qui va nous permettre de créer un device mapper dès le démarrage du kernel.

=> setenv bootargs $bootargs dm-mod.create=\"vroot,,,ro, \
0 314040         \
verity           \
1                \
/dev/mmcblk0p2   \
/dev/mmcblk0p3   \
1024 4096 157020 \
1                \
sha256           \
b9084c496613f66036108a12cfa0b3dc64ff32c46ad9d3d960280ace0c57fb11\
bc00e89071c1ef9cc0ba337bb4c0ce240af15ba7070fa30cbf611af7f8bda325
"; 

Les paramètres de dm-mod.create sont définis comme suit: 

dm-mod.create=<name>,<uuid>,<minor>,<flags>,<table>

<table> = <start> <size> <target_type> <version> <data_dev> <hash_dev> <data_block_size> <hash_block_size> <num_data_blocks> <hash_offset> <hash_algorithm> <root_hash> <salt>

On peut observer ces logs intéressants lors du démarrage : 

[...]
[2.917973] device-mapper: verity: sha256 using implementation "sha256-caam"
[2.925977] device-mapper: ioctl: dm-0 (vroot) is ready
[...]

La première ligne informe que la vérification des empreintes est accélérée par le CAAM présent sur la carte et la deuxième ligne nous informe que le device mapper a bien été créé. 

d) Test de corruption

Nous allons maintenant tester si notre implémentation fonctionne correctement.

Dans un premier temps, le device mapper ne doit être qu’en lecture seule :

~$/ sudo echo test > vroot

-sh: echo: write error: Operation not permitted

Puis, nous allons tester si le device mapper détecte bien une corruption des données.

Pour cela, nous avons créé une partition vide avec le mot « test » au début, puis nous avons fait toutes les manipulations précédentes avec la génération de l’arbre de Merkle. Nous avons ensuite corrompu la partition contenant les données en y écrivant une donnée sans passer par le device mapper :

~$/ echo corrupt | dd of=/dev/mmcblk0p3 bs=1M seek=10

0+1 records in
0+1 records out
8 bytes copied, 0.000542625 s, 14.7 kB/s

On peut donc observer le contenu de la partition contenant les données :

~$/ hexdump /dev/mmcblk0p3 -C

00000000  74 65 73 74 0a 00 00 00  00 00 00 00 00 00 00 00  |test............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00a00000  63 6f 72 72 75 70 74 0a  00 00 00 00 00 00 00 00  |corrupt.........|
00a00010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
03300000

On va maintenant lire les données de la partition de données en passant par le device mapper (dm-verity) cette fois-ci et voir s'il détecte bien la corruption des données :

~$/ hexdump /dev/mapper/vroot

0000000 6574 7473 000a 0000 0000 0000 0000 0000
0000010 0000 0000 0000 0000 0000 0000 0000 0000
*
[  955.085613] device-mapper: verity: 179:3: data block 2560 is corrupted
[  955.094629] device-mapper: verity: 179:3: data block 2560 is corrupted
[  955.101249] Buffer I/O error on dev dm-0, logical block 2560, async page read
hexdump: vroot: Input/output error
0a00000

L'erreur d'I/O remontée par le kernel est l'erreur produite par dm-verity lorsque l'on tente de lire un bloc dont la signature n'est pas correcte, la corruption est donc bien détectée.

Conclusion 

Pour conclure, nous avons vu les éléments nécessaires pour implémenter le démarrage sécurisé sur la plateforme iMX8 : en authentifiant chaque maillon de la chain-of-trust résumé dans la figure suivante :

Chain-of-trust

Après avoir implémenté cette chain-of-trust il est désormais possible d'automatiser certaines étapes pour obtenir cette chaîne finale plus facilement. Nous pouvons automatiser la génération des différentes images avec les bonnes configuration à l'aide de Yocto.

Notre implémentation (de démonstation) n’est pas totalement imperméable aux attaques et pour régler cela nous devons encore : authentifier les variables d’environnements U-Boot (qui contiennent le roothash de dm-verity) et rendre systématique la vérification des signatures au démarrage (pas d'interventions manuelles).

Bibliographie

https://boundarydevices.com/high-assurance-boot-hab-i-mx8m-edition/

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.