Introduction
Ce guide décrit l'installation d'un système Debian GNU/Linux sur un disque entièrement chiffré, avec stockage de la clé de chiffrement dans le TPM (Trusted Plateform Module) du PC, avec support de SecureBoot. L'utilisation combinée du TPM et de SecureBoot permet de déchiffrer le disque au démarrage sans interaction utilisateur (pas de saisie de phrase de passe, ni de dispositif externe à connecter) tout en garantissant que la clé de déchiffrement ne sera fournie qu'au système approuvé : celui que nous allons installer.
L'installation présentée ici se base sur des choix différents de ceux de l'installateur Debian officiel, ce qui impliquera des différences de configuration, mais l'ensemble de ce qui sera installé provient bien des dépôts de paquets de Debian, et les mises à jour se feront de la manière habituelle avec les utilitaires Debian.
Matériel nécessaire
Cette installation nécessite un PC avec micrologiciel UEFI et support de SecureBoot, et équipé d'un module TPM 2.0. Ces caractéristiques étant exigées par Microsoft, tout PC commercialisé avec Windows 10 ou supérieur devrait en principe les satisfaire.
Il n'y a pas besoin de support spécifique à l'installation, mais l'essentiel des manipulations devront se faire depuis un système GNU/Linux opérationnel sur la machine de destination. Un système live ou un système déjà installé sur la même machine (mais sur un autre disque) peuvent convenir. L'ensemble des commandes sont à exécuter en tant que root, cela n'est donc pas systématiquement précisé.
Sources
Le contenu de ce guide est en grande partie basé sur plusieurs sources anglophones :
- https://blastrock.github.io/fde-tpm-sb.html : la source principale, l'organisation générale est la même et certains scripts sont repris quasiment tel quel. Certains passages sont des quasi-traductions, d'autres sont plus éloignés, et je n'ai pas repris certains aspect comme la gestion de l'hibernation, dont je n'ai personnellement pas l'usage.
- https://www.rodsbooks.com/efi-bootloaders/controlling-sb.html : source référencée par la précédente, utilisée principalement pour la partie Secure Boot, en particulier la génération des clés.
- Le wiki de la distribution arch linux, et en particulier cette page.
- https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock : article présentant une vulnérabilité qui affecte la plupart des installations avec déchiffrement automatisé depuis le TPM. Lecture fortement recommandée.
D'une manière générale, il est tout à fait possible que j'aie introduit des erreurs qui ne sont pas présentes dans ces sources.
Partitionnement du disque
La première étape consiste à préparer le disque pour l'installation en y créant les partitions nécessaires. À ce stade, il n'est pas indispensable de travailler sur la machine cible, il est tout à fait possible de partitionner le disque depuis un autre PC. Il faudra toutefois faire attention au fait que le nommage du périphérique peut être différent selon la machine et le mode de connexion. On suppose ici que l'installation se fait sur un unique disque, vierge au départ, et que l'on ne souhaite pas y installer d'autre système.
Avant tout, il faut identifier le disque sur lequel on va travailler. La commande lsblk permet de
voir l'ensemble des disques présents sur le système, ainsi que l'arborescence de leurs partitions.
Dans mon cas, j'installe le système sur l'unique disque NVMe d'un PC portable. Le périphérique est
donc nommé /dev/nvme0n1, c'est ce nom qui sera utilisé dans l'ensemble des exemples.
Création de la table de partition
On utilise fdisk pour créer les partitions :
fdisk /dev/nvme0n1
Puis on suit les étapes suivantes :
- créer une table de partition de type GPT (commande
g) - créer une première partition de type EFI de 512 Mio (on verra plus loin pourquoi cette taille est
nécessaire) :
- commande
npour créer la partition, - numéro de partition et premier secteur par défaut (valider sans rien entrer),
- pour le dernier secteur saisir
+512M, - définir le type de partition : commande
tpuis saisir la valeur1.
- commande
- créer une seconde partition qui contiendra l'ensemble du système :
- commande
npour créer la partition, - utiliser l'ensemble de l'espace restant en validant toutes les questions sans rien saisir,
- le type par défaut Linux filesystem convient, on ne le modifie pas.
- commande
- quitter fdisk en enregistrant les changements avec la commande
w.
Création du système de fichier EFI
Il reste à créer le système de fichier de la partition EFI, qui doit être de type FAT 32. On le fait avec la commande suivante :
mkfs.vfat -F 32 /dev/nvme0n1p1
Création de la partition système
Pour accueillir dans cette unique partition l'ensemble du système, le tout étant chiffré, il va falloir superposer plusieurs couches logicielles. La mise en place se divise donc en plusieurs étapes, correspondant à ces différentes couches :
- une partition LUKS (Linux Unified Key Setup) qui servira de conteneur chiffré pour les couches supérieures,
- une couche LVM (Logical Volume Management) pour pouvoir créer plusieurs volumes logiques dans un conteneur LUKS unique,
- un système de fichier dans chaque volume logique.
Avant de détailler ces étapes, assurons-nous que nous avons bien installé tous les programmes nécessaires :
apt install cryptsetup lvm2 btrfs-progs
Contrairement à ce que l'on pourrait penser, il est parfaitement possible d'installer des paquets supplémentaires sur un système live, cette installation ne sera simplement pas persistente.
Création du conteneur chiffré
On commence par créer la partition LUKS avec la commande suivante :
cryptsetup luksFormat --type luks2 --verify-passphrase /dev/nvme0n1p2
Notez qu'à ce stade, il n'est pas encore question de l'utilisation du TPM. La clé de chiffrement est protégée, de façon classique, par une phrase de passe qu'il faudra saisir pour déchiffrer la partition. Le déchiffrement automatique par le TPM sera mis en place plus tard car cela doit être fait depuis le système que nous nous apprêtons à installer, et donc lorsque celui-ci sera opérationnel et que nous aurons pu démarrer dessus.
Même si elle ne sera plus utilisée au quotidien, la phrase de passe définie ici restera toutefois en place et servira de clé de secours, en cas de problème. Par exemple si le système ne démarre plus et que l'on a besoin de démarrer depuis un autre support (une clé USB live, par exemple). Il est donc important de bien la choisir : elle ne doit pas être trop simple pour ne pas affaiblir le chiffrement du disque, mais il faudra pouvoir s'en souvenir, alors même qu'on ne l'utilisera pas au quotidien.
On va ensuite ouvrir la partition LUKS :
cryptsetup open /dev/nvme0n1p2 nvme0n1p2_crypt
Le dernier paramètre de cette commande est un nom totalement arbitraire que l'on donne au volume déchiffré. Du point de vue du fonctionnement de LUKS, ce nom n'est pas enregistré et pourrait donc changer à chaque nouvelle ouverture. Il sera toutefois utilisé plus loin dans une configuration persistente, il faut donc choisir tout de suite un nom qui sera conservé ensuite. Utiliser le nom du volume sous-jacent suffixé par _crypt semble être une convention courante, mais cela n'a rien d'obligatoire.
Le contenu en clair de la partition est désormais accessible dans /dev/mapper/nvme0n1p2_crypt.
Mise en place de la gestion de volumes logiques
L'étape suivante est d'initialiser la gestion des volumes logiques. Pour cela, on va d'abord définir notre partition en tant que Volume Physique (PV, d'après les initiales anglaises) :
pvcreate /dev/mapper/nvme0n1p2_crypt
On crée ensuite un Groupe de Volumes (VG) constitué de notre unique PV. Contrairement au PV, nous devons nommé notre VG. Là encore le nom est arbitraire, mais choisir une convention de nommage et s'y tenir permet de s'y retrouvé par la suite. C'est d'autant plus important que l'on ne manipulera pas ce nom quotidiennement, il faut donc choisir un nommage qui sera facile à comprendre le jour ou on devra y remettre le nez après plusieurs mois ou années. Sur un PC portable ou de bureau, il est assez peu probable d'avoir plusieurs VG sur la même machine, je nomme donc tout simplement mes VG d'après le nom d'hôte préfixé par vg_. Si la machine doit s'appeler, par exemple, rocannon, cela donne :
vgcreate vg_rocannon /dev/mapper/nvme0n1p2_crypt
On peut maintenant créer les Volumes Logiques (LV) qui accueilleront les différents systèmes de fichiers dont nous avons besoin. Le nombre de LV dépend du choix du type de système de fichier : j'utilise ici une unique partition Btrfs (qui sera organisée en sous-volumes à l'étape suivante) et une partition d'échange (swap). Je crée donc les deux LV correspondant :
lvcreate -n lv_swap -L 8G vg_rocannon
lvcreate -n lv_racine -l 100%FREE vg_rocannon
Là encore, les noms sont totalement arbitraires. La remarque faite plus haut sur la pertinence de suivre une convention de nommage cohérente reste valable, ici j'utilise le préfixe lv_ suivi d'un mot décrivant l'usage du volume.
Notez la nuance entre les options -L, qui indique la taille absolue, et -l, qui permet
d'exprimer une taille en fonction de l'espace libre restant (et aussi d'autres possibilités, cf. la
page de manuel de lvcreate).
Je ne traite pas ici de la question de la pertinence d'une partition de swap sur une machine moderne avec beaucoup de mémoire physique, ni de la question de sa taille. Sur ce sujet on peut se référer, entre autre, à cet article.
Création des systèmes de fichiers
Ça n'est pas, à proprement parler, un système de fichier, mais la création de l'espace d'échange vient assez naturellement à ce stade du processus :
mkswap /dev/vg_rocannon/lv_swap
On crée ensuite le système de fichier btrfs :
mkfs.btrfs /dev/vg_rocannon/lv_racine
Un des avantages de Btrfs est la possibilité de créer des sous-volumes au sein du même système de
fichier. Je trouve ce sytème plus flexible que LVM pour séparer, par exemple, /home de la racine
/. C'est la raison pour laquelle j'ai créé un seul LV avec un seul système Btrfs. Nous alons
maintenant créer les sous-volumes, ce qui nécessite d'abord de monter (temporairement) la partition.
On le fait ici directement dans le /mnt du système depuis lequel on travaille :
mount /dev/vg_rocannon/lv_racine /mnt
cd /mnt
btrfs subvolume create racine
btrfs subvolume create home
umount /mnt
Je n'ai créé ici de sous-volume que pour la racine et /home. Si vous souhaitez séparer de la
racine d'autres points de montage, comme /usr, /var ou /tmp, il faut aussi leur créer chacun
son sous-volume. On ne fixe pas de taille à ce stade, mais Btrfs permet de fixer des quotas pour
les sous-volumes, je ne détaille pas cet aspect dans ce guide, cela est bien documenté, par exemple
dans la page de manuel de la commande btrfs-quota.
Bilan d'étape
Cette première phase de préparation est maintenant terminée, et nous allons pouvoir passer à l'installation proprement dite. Avant cela, je discute un peu plus, ci-dessous, des choix faits en terme de partitionnement. Vous pouvez sans problème sauter cette partie et passer au chapitre suivant.
On pourrait s'attendre à ce que le choix d'utiliser les sous-volumes de Btrfs permette de se passer totalement de la couche LVM, ce qui aurait simplifié l'ensemble. C'est uniquement la partition d'échange qui rend ici LVM nécessaire. Plusieurs solutions auraient permis d'éviter cela, mais aucune ne me semble satisfaisante :
- la première est de mettre la swap dans sa propre partition directement sur le disque mais il aurait alors fallu lui créer son propre volume LUKS. En effet, un système chiffré mais avec une swap en clair ne servirait plus à rien : il serait tout à fait possible que des données sensibles, (y compris la clé de chiffrement du volume LUKS) aterrissent dans la swap, et soient donc accessibles en clair. Cela aurait aussi rendu moins flexible une éventuelle modification de la taille de la swap, et le processus de déchiffrement au démarrage aurait été un peu plus complexe.
- la seconde est de ne pas avoir de swap du tout, mais d'après l'article cité plus haut (dont l'auteur est un des principaux développeurs du noyau Linux…), ça n'est pas une bonne idée. Mais si vous choisissez de vous passer totalement de swap, vous pouvez alors sauter l'étape LVM et créer directement le système Btrfs à la place du PV.
- la troisième est d'utiliser un fichier de swap, plutôt qu'une partition. C'est sans doute une
solution beaucoup plus flexible : non seulement elle nous aurait permit d'éviter la couche LVM,
mais elle permet de modifier plus facilement la taille de la swap si nécessaire. Malheureusement,
les fichiers de swap s'accomodent mal du system de Copy-on-Write qui est à la base de Btrfs
(voir à ce sujet la documentation de
Btrfs). Choisir un autre système de
fichier, comme ext4, aurait permit d'utiliser un fichier de swap, mais cela aurait aussi
nécessité de réintroduire LVM pour pouvoir séparer
/homede la partition racine. Et même dans ce cas, les fichiers de swap ont la réputation d'être moins performants, mais surtout moins largement utilisés, donc moins testés et moins fiables.
Système de base
L'installation du système de base de Debian se fait grace au script debootstrap, fourni par le paquet du même nom. Pour l'utiliser, nous devons d'abord monter les deux sous-volumes Btrfs créés à l'étape précédente, en respectant la hierarchie des répertoires.
Préparation des répertoires
On commence par le système racine :
mount -o subvol=racine /dev/vg_rocannon/lv_racine /mnt
Notez l'option subvol qui fait qu'on ne monte pas l'ensemble du volume Btrfs, mais seulement le
sous-volume spécifié. On va ensuite créer le répertoire /home à la racine de ce sous-volume, pour
ensuite y monter le sous-volume correspondant :
mkdir /mnt/home
mount -o subvol=home /dev/vg_rocannon/lv_racine /mnt/home
Nous avons désormais nos sous-volumes montés dans /mnt conformément à la structure de répertoires
classique. Si vous avez créé d'autres sous volumes pour /var, /usr ou /tmp, il est temps de
les monter de la même façon. En fait, l'installation du système de base ne créant en principe rien
dans /home, le monter tout de suite n'était pas forcément indispensable. C'est par contre
obligatoire pour /usr ou /var. Concernant /tmp, j'ai l'habitude d'en faire un système de
fichier en mémoire, je le crée donc tout de suite, pour éviter que d'éventuels fichiers temporaires
de l'installation ne viennent se créer sur le disque, et soient ensuite masqués par le montage du
tmpfs. Là encore, ça n'est probablement pas indispensable à ce stade.
mkdir /mnt/tmp
mount -t tmpfs -o size=2G tmpfs /mnt/tmp
On monte enfin la partition EFI à son emplacement habituel :
mkdir -p /mnt/boot/efi
mount /dev/nvme0n1p1 /mnt/boot/efi
Installation du système de base
Pour l'installation, on va simplement indiqué à debootstrap la version de Debian à utiliser, le répertoire cible (correspondant à la racine du futur système), et enfin l'adresse du dépôt depuis lequel télécharger les paquets :
debootstrap trixie /mnt https://deb.debian.org/debian/
À l'heure ou j'écris ces lignes, Debian Trixie est encore la version de test, mais certains outils utilisés ici ne sont pas disponible dans la version stable actuelle (Bookworm). L'installation décrite ici ne sera donc probablement possible pour une Debian stable qu'une fois que Trixie aura été publiée en tant que version stable Debian 13.
Configuration minimale
Maintenant que le système de base est installé, nous allons pouvoir y exécuter un shell grâce à chroot, et y créer la configuration minimale nécessaire pour pouvoir ensuite le démarrer.
Préparation du chroot
En plus des fichiers créés par debootstrap, le système a besoin d'un certain nombre de fichiers
qui n'existent pas réellement sur le disque, mais représentent, entre autre, les moyens d'accès au
matériel ou divers états interne du système. Il s'agit du contenu des répertoires /proc, /dev,
/sys et /run, ainsi que certains de leurs sous-répertoires. Nous allons donc monter ces
répertoires dans le système en cours d'installation depuis le système hôte :
mount -t proc none /mnt/proc
mount -o bind /dev /mnt/dev
mount -o bind /dev/pts /mnt/dev/pts
mount -o bind /sys /mnt/sys
mount -o bind /sys/firmware/efi/efivars /mnt/sys/firmware/efi/efivars
mount -o bind /run /mnt/run
Nous avons désormais une hierarchie de répertoires complète dans /mnt, et nous pouvons donc y
démarrer un shell avec chroot :
chroot /mnt /bin/bash
Configuration
Nous sommes maintenant dans un bash qui s'exécute dans le contexte de notre système en cours d'installation, nous allons donc pouvoir mettre en place la configuration minimale nécessaire au démarrage.
On commence par installer un certain nombre de paquets nécessaires au démarrage. J'y ajoute neovim, par préférence personnelle, l'important est d'avoir un éditeur qui vous convient (par défaut c'est nano qui est installé avec Debian) :
apt-get update
apt-get install cryptsetup lvm2 btrfs-progs adduser console-setup locales neovim
Montage des systèmes de fichiers
Le principal point de configuration à ce stade est le montage des systèmes de fichiers que nous
avons créés, on va d'abord définir notre volume chiffré dans /etc/crypttab avec la ligne suivante
:
# <target name> <source device> <key file> <options>
nvme0n1p2_crypt /dev/nvme0n1p2 none luks,discard
Maintenant que le volume chiffré est défini, on peut configurer les différents systèmes de fichiers
dans /etc/fstab :
# <file system> <mount point> <type> <options> <dump> <pass>
/dev/vg_rocannon/lv_racine / btrfs defaults,subvol=racine 0 1
/dev/vg_rocannon/lv_racine /home btrfs defaults,subvol=home 0 2
/dev/nvme0n1p1 /boot/efi vfat umask=0077 0 1
/dev/vg_rocannon/lv_swap none swap sw 0 0
tmpfs /tmp tmpfs defaults,size=2g 0 0
Dans ces deux fichiers, on a ici identifié les périphériques (deuxième colonne de crypttab et
première de fstab) par leur chemin dans /dev, mais il est préférable de les identifier par leur
UUID, les numéros dans les noms de périphériques étant susceptibles de changer en cas d'ajout ou de
retrait de matériel (les noms de volumes LVM sont toutefois plus stables).
Une façon pratique d'obtenir la valeur de l'UUID au bon format est d'utiliser la commande suivante :
blkid | grep 'crypto_LUKS' | cut -d ' ' -f 2 | tr -d '"'
La valeur recherchée par grep est bien sûr à adapter. La chaîne crypto_LUKS utilisée ici
retournera le (ou les, s'il y a plusieurs volumes chiffrés) UUID à utiliser dans le fichier
crypttab.
Création des comptes utilisateurs
L'installation de base par debootstrap ne crée aucun compte utilisateur, et ne configure pas non plus de mot de passe pour le compte root. Au démarrage, nous serions donc dans l'impossibilité de nous connecter. Il n'est pas indispensable de créer tout de suite l'ensemble des comptes utilisateurs dont on aura besoin au final, mais il faut a minima disposer d'un compte avec les droits d'administration. Deux choix sont possibles :
- soit créer un mot de passe pour root, il suffit pour cela d'utiliser la commande
passwd, - soit créer un compte utilisateur avec
adduser, et lui donner les droits d'administration en l'ajoutant au groupe sudo :adduser toto apt-get install sudo adduser toto sudo
Mise en place des clés Secure-Boot
Secure-Boot est une fonctionnalité associée à UEFI qui vise à ne permettre de démarrer un système que s'il est signé par une clé cryptographique connues du micrologiciel.
Le mode d'installation par défaut de Debian (et de la plupart des autres distributions) repose, pour le démarrage, sur un enchaînement de plusieurs étapes pour aboutir au démarrage d'un noyau Linux qui n'est pourtant pas directement signé par une clé acceptée par Secure-Boot. Le premier élément à être chargé est nommé Shim, il s'agit d'un chargeur d'amorçage (ou bootloader) minimal qui a pour fonction de passer le relais à un autre exécutable UEFI, après avoir vérifier qu'il était autorisé. Shim est autorisé à démarrer parce qu'il est signé par Microsoft, et que la clé publique correspondant à la signature est installée par défaut par tous les principaux constructeurs. Il embarque à son tour les clés des principales distributions Linux, ce qui lui permet de vérifier les différents systèmes qu'il charge dans un deuxième temps. Dans la plupart des cas, Shim charge un second chargeur d'ammorçage, plus généraliste, comme Grub, qui charge à son tour le noyau Linux.
Le fonctionnement proposé ici est assez différent : l'idée est de prendre véritablement possession de notre machine en y installant nos propres clés, ce qui nous permettra ensuite de signer nous même directement notre noyau, et donc de le démarrer directement.
Organisation des différents types de clés
Secure-Boot est basé sur la cryptographie asymétrique : les clés vont par deux, l'une privée l'autre publique. La clé privée sert à signer des fichiers (exécutables UEFI, autres clés...) et la clé publique associée permet de vérifier la signature. Le micrologiciel UEFI n'ayant pas pour fonction de signer quoi que ce soit, mais seulement de vérifier des signatures, il ne comporte donc que des clés publiques. Ces clés sont stockées dans différentes variables UEFI :
- db, la base de données des clés : elle contient les clés publiques qui sont utilisés pour vérifier les systèmes et autoriser leur démarrage. Les clés privées associées sont donc celle qui doivent être utilisées pour signer les exécutables.
- dbx, la base de données d'interdiction : elle peut contenir des clés qui ont été révoquées (suite à une compromission de la clé privée associée, par exemple) mais aussi des valeurs de hachages correspondant soit à des programmes malveillants qui serait parvenus à être signés par des clés légitimes, soit à des logiciels légitimes, et donc signés, dans lesquels auraient été découvertes des vulnérabilités qui permettrait de les détourner à des fins malveillantes.
- KEK (pour Key Exchange Key), les clés d'échange de clés : il s'agit de clés dont le rôle est de vérifier les mises à jour de db et de dbx.
- PK (pour Platform Key), la clé de plateforme : il s'agit de la clé de plus haut niveau, émise en principe par le constructeur de la machine, et qui permet de vérifier les mises à jour des clés d'échange de clés. Contrairement aux précédentes, cette clé est nécessairement unique.
Ces différents types de clés organisent donc une hierarchie dans laquelle chaque niveau a le pouvoir de modifier le niveau strictement inférieur. Seule exception : la PK, étant au plus au niveau, peut se mettre à jour elle-même, c'est à dire qu'une nouvelle PK sera acceptée, et remplacera la PK actuelle, si elle est vérifiée par cette dernière.
Extraction des clés existantes
Même si le système installé sera signé par notre propre clés, il peut être utile de conserver les clés installées par défaut, ou du moins une partie d'entre elles. Par exemple, pour pouvoir démarrer une image live téléchargée depuis le site d'une distribution, il faut conserver la clé Microsoft utilisée pour signer shim.
Pour cela, nous allons commencer par extraire l'ensemble des clés installées par défaut. Cela permet d'une part de les sauvegarder, pour pouvoir les restaurer en cas de problème, et d'autre part d'inclure celles que nous souhaitons conserver dans l'ensemble de clés que nous installeront à l'étape suivante. À partir de maintenant, nous allons interagir avec l'UEFI, puis avec le TPM, il est donc désormais impératif de travailler sur la machine cible.
Les clés étant stockées dans des variables UEFI, leur lecture se fera avec la commande
efi-readvar, fournie par le paquet efitools. On indique le nom de la variable à lire avec
l'option -v et le fichier de sortie avec -o (sans cette option, on obtient une description du
contenu sur la sortie standard). Les fichiers sont au format esl pour Efi Signature
List.
efi-readvar -v PK -o PK.esl
efi-readvar -v KEK -o KEK.esl
efi-readvar -v db -o db.esl
efi-readvar -v dbx -o dbx.esl
Chaque fichier contient donc une liste de signature (même si par principe la liste PK ne comporte
qu'un seul élément). Pour pouvoir gérer les différentes clés de façon individuelle, on va maintenant
convertir ces listes en fichiers unitaire, au format OpenSSL, grâce à la commande
sig-list-to-certs, également fournie par le paquet efitools :
sig-list-to-certs KEK.esl KEK
sig-list-to-certs db.esl db
Le premier argument est le nom du fichier, et le second le nom de base pour les fichiers créés. On ne converti pas la PK, puisque celle-ci est nécessairement unique, et que nous allons la remplacer par la nôtre. Il n'est probablement pas utile non plus de convertir le contenu de dbx car nous n'avons rien à y ajouter et qu'il est certainement préférable de conserver l'ensemble de son contenu. Ça n'est donc utile que pour KEK et db, dans le cas où vous souhaitez n'en conserver qu'une partie, par exemple pour conserver la clé avec laquelle Microsoft signe shim, et donc indirectement les différentes distributions Linux, mais pas celle qui sert à signer Windows.
Les clés sont extraites au format DER, nous allons les convertir au format PEM dont nous aurons besoin à l'étape suivante. Cela peut aussi être l'occasion de renommer les fichiers pour mieux s'y retrouver. Par exemple, si la seconde KEK (numérotée 1, du fait du démarrage à 0) est celle de Microsoft :
openssl x509 -in KEK-1.der -out KEK-Microsoft.crt -outform PEM
Génération des clés
Nous allons maintenant générer nos propres clés UEFI, qui nous permettront de signer notre noyau. Il y a 3 clés à générer, une pour chaque type : PK, KEK et db. La génération se fait avec OpenSSL, par exemple pour la PK :
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=<nom-de-la-clé>/" \
-keyout PK.key -out PK.crt -days 10950 -noenc
Le nom indiqué après CN= permettra d'identifier la clé, il sera affiché dans le menu de l'UEFI ou
dans la sortie de la commande efi-readvar. Vous pouvez indiquer ce que vous voulez, tant que vous
vous y retrouvez. À titre d'exemple, j'indique mon nom pour les PK et KEK, et le nom de la machine
pour la db, et je suffixe dans tous les cas par le type de clé (PK, KEK ou db).
Les clés privées (le fichier PK.key dans l'exemple ci-dessus) doivent impérativement être conservées
en sureté. Idéalement, elles devraient rester sur la machine qui les a générées, avec des droits
d'accès restreint. Une bonne façon de faire est de les générer dans un sous-répertoire dédié de
/root et de restreindre tous les droits sur le répertoire et sur les fichiers de clés privées au
seul propriétaire (qui est donc root).
À noter que l'UEFI n'a besoin que des clés publiques. Pour signer le noyau lors de son installation, la clé privée db devra être présente sur le système cible. En revanche, les clés privées PK et KEK ne sont nécessaires que lors de la mise en place initiale (et lors d'une éventuelle future mise à jour de clé), elles peuvent donc être générées sur une autre machine, et seules les clés publiques auront besoin d'être transférer sur le système en cours d'installation.
Si vous comptez suivre cette procédure pour plusieurs machines, il est donc possible de ne générer qu'une seule PK et KEK pour l'ensemble. Il faudra en revanche créer une clé db pour chaque machine (il serait fondamentalement possible d'utiliser la même clé sur plusieurs machines, mais cela impliquerait de copier la clé privée sur chaque machine, ce qui signifie qu'en cas de compromission d'une des machines, il faudrait changer la clé sur toutes les autres).
Maintenant que nous avons créé nos clés, il faut les convertir au format esl qui est celui attendu
par l'UEFI. On commence par convertir individuellement chaque clé publique avec
cert-to-efi-sig-list. Cette commande admet une option -g qui permet de fournir un identifiant de
propriétaire de clé, au format UUID. On va donc d'abord générer, avec la commande uuid cet
identifiant, que l'on utilisera ensuite pour l'ensemble de nos clés.
guid=$(uuid)
cert-to-efi-sig-list -g $guid PK.crt PK.esl
Pour la PK, qui est par principe unique, il n'y a rien d'autre à faire. Mais pour les KEK et db, nous allons obtenir un fichier esl pour chaque clé. Le format esl permet de faire cela très simplement, il suffit de concaténer les fichiers :
cat KEK1.esl KEK2.esl KEK3.esl > KEK.esl
Si vous souhaitez conserver toutes les clés préinstallées, et simplement ajouter la vôtre, vous
pouvez donc concaténer simplement votre fichier esl avec celui extrait précédement avec
efi-readvar. Si par contre vous souhaitez sélectionner seulement certaines clés, il faudra
re-convertir les certificats obtenus avec sig-list-to-certs en fichier esl individuel, puis les
concaténer. Dans ce cas, il est probablement préférable, en tout cas plus rigoureux, de conserver
pour ces fichiers le guid propriétaire d'origine. Celui-ci est afficher dans la sortie par défaut
(sans option -o) de la commande efi-readvar.
Signature des clés
Nous avons désormais nos 3 fichiers de clés : PK.esl, KEK.esl et db.esl. Il nous reste à les signer selon la hierarchie présentée plus haut : la PK est signée par elle même, la KEK par la PK, et la db par la KEK. Les fichiers signés sont nommés avec l'extension .auth :
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" -k PK.key -c PK.crt PK PK.esl PK.auth
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" -k PK.key -c PK.crt KEK KEK.esl KEK.auth
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" -k KEK.key -c KEK.crt db db.esl db.auth
Nos clés sont maintenant prêtes à être installées, mais avant cela nous devons installer le noyau pour que notre système soit en capacité de démarrer.
Mise en place du noyau
La dernière (mais pas la moindre) chose qu'il manque à notre système, c'est un noyau.
Notre noyau sera installé depuis le paquet debian ordinaire, mais celà ne suffit pas : le noyau est
installé directement dans /boot, qui dans notre installation se trouve dans le volume chiffré. Il
ne sera donc pas accessible lors du démarrage et ne pourra donc pas être chargé. Nous allons donc
mettre en place un système permettant, à partir du noyau packagé par Debian, d'avoir un fichier
amorçable par l'UEFI.
Création d'une image de noyau unifiée (UKI) amorçable
Pour permettre le démarrage du système, nous allons regrouper tout ce qui est nécessaire, c'est à dire principalement le noyau Linux et son système de fichier initial (initrd), en un seul fichier directement amorçable par l'UEFI, qui sera installé sur la partition EFI. Ce type de fichier est connu sous le nom d'UKI, pour Unified Kernel Image ou, en français, image de noyau unifiée. Pour plus de détails sur ce format, on pourra consulté, par exemple, le wiki Arch. Ce fichier sera signé avec notre clé SecureBoot mise en place à l'étape précédente, ce qui lui permettra d'être chargée directement, sans passer par un chargeur d'amorçage.
On commence par installer les paquets dont nous aurons besoin pour cette étape :
apt-get install systemd-ukify sbsigntool efibootmgr systemd-boot-efi cryptsetup-initramfs
Pour que cette génération se fasse automatiquement à chaque mise à jour du noyau, nous allons la
mettre en place dans un script placé dans le répertoire /etc/kernel/postinst.d/ qui sera donc
exécuté après chaque installation d'un noyau. Nous allons donc créer un fichier
/etc/kernel/postinst.d/zz-1-update-uki (le préfixe permet de contrôler l'ordre d'exécution, dans
le cas où d'autres scripts seraient présents) avec le contenu suivant :
#!/bin/sh -e
version="$1"
/usr/lib/systemd/ukify build \
--linux /boot/vmlinuz-${version} \
--initrd /boot/initrd.img-${version} \
--os-release @/usr/lib/os-release \
--uname ${version} \
--cmdline @/etc/cmdline \
--output /boot/efi/EFI/debian/Linux-${version}.efi
/usr/bin/sbsign \
--key /root/Rocannon-SecureBoot.key \
--cert /root/Rocannon-SecureBoot.crt \
--output /boot/efi/EFI/debian/Linux-${version}.efi \
/boot/efi/EFI/debian/Linux-${version}.efi
Ce script fait référence à plusieurs fichiers externes qui doivent donc être présents et dont les chemins sont potentiellement à adapter :
/usr/lib/os-releaseest installé par le paquetbase-files, qui fait partie de l'installation de base,/etc/cmdlineest un fichier que nous allons créer et qui nous permet de configurer la ligne de commande de démarrage du noyau. Il se compose de la ligne suivante (à adapter, en particulier selon le nom du volume group choisi lors du partitionnement) :root=/dev/mapper/vg_rocannon-lv_racine ro rootflags=subvol=racine quiet loglevel=3 panic=0/root/Rocannon-SecureBoot.keyet/root/Rocannon-SecureBoot.crtcorrespondent respectivement à notre clé privée de type db, générée à l'étape précédente, et à son certificat de clé publique.
Notre UKI ne contient pas seulement le noyau, mais également l'initrd associé. Elle doit donc
également être régénérée en cas de mise à jour de l'initrd. Pour cela, il nous suffit de créer un
lien symbolique vers notre script dans /etc/initramfs/post-update.d/ :
mkdir -p /etc/initramfs/post-update.d
ln -s /etc/kernel/postinst.d/zz-1-update-uki /etc/initramfs/post-update.d/
Installation de l'UKI dans l'UEFI
Une fois notre image créée et signée dans la partition EFI, il faut encore créer une entrée
correspondante dans la séquence de démarrage de l'UEFI. Cela se fait avec un second script,
également dans /etc/kernel/postinst.d/, que nous allons nommer zz-2-install-uki, pour garantir
qu'il s'exécute bien après le précédent :
#!/bin/sh -e
version="$1"
/usr/bin/efibootmgr \
--disk /dev/nvme0n1 \
--create \
--label "Debian-Linux-${version}" \
--loader "\EFI\debian\Linux-${version}.efi"
Contrairement au précédent, ce script n'a besoin d'être exécuté que lors de la première installation
de chaque version du noyau, il ne faut donc pas créer de lien dans /etc/initramfs/post-update.d/.
Gestion de la désinstallation
Pour éviter de saturer notre partition EFI, il faut également supprimer les fichiers que nous y
avons créés lors de la désinstallation du noyau correspondant, et mettre à jour la configuration
UEFI en conséquence. De façon similaire à l'installation, nous allons mettre en place un script dans
le répertoire /etc/kernel/postrm.d/, nommé zz-remove-uki :
#!/bin/sh -e
version="$1"
# recherche du numéro de l'entrée concernée dans l'ordre de démarrage UEFI
num=$(efibootmgr --disk /dev/nvme0n1 \
| grep "Debian-Linux-${version}" \
| cut -c 5-8)
if [ -n "$num" ]
then
efibootmgr \
--disk /dev/nvme0n1 \
--delete-bootnum \
--bootnum $num
fi
# l'option -f évite une erreur si le fichier n'existe pas
rm -f /boot/efi/EFI/debian/Linux-${version}.efi
Installation du noyau
Maintenant que nos scripts sont bien en place, nous pouvons installer le meta-paquet correspondent à l'architecture utilisée :
apt-get install linux-image-amd64
La sortie de la commande d'installation, et l'examen du contenu de la partition EFI, permettent de vérifier que tout s'est bien passé.
Installation des clés et déchiffrement par le TPM
Tout est maintenant en place, et nous allons pouvoir installer nos clés dans l'UEFI, puis démarrer sur notre système nouvellement installé.
Installation des clés
La mise en place initiale des clés se fait dans l'interface UEFI, il faut donc placer les clés à
installer (les trois fichiers .auth généré précédemment) dans un emplacement accessible depuis
l'UEFI. Le plus simple est en général de les copier à la racine de la partition EFI. Dans certains
cas il est également possible d'utiliser un support amovible comme une clé USB. Il faut ensuite
redémarrer la machine, et entrer dans le menu de configuration EFI. Cela peut se faire soit en
appuyant sur une touche au démarrage (souvent Suppr ou F2, parfois F1 ou d'autre encore), soit
directement depuis un système déjà démarré en utilisant la commande suivante :
systemctl reboot --firmware
Le procédé exact d'installation des clés, et en particulier les libellés utilisés dans les menus, varient selon les constructeurs. Mais la procédure générale est toujours la même : il faut d'abord supprimer la Platform Key existante, pour pouvoir ensuite installer nos clés. Un système sans PK est dit en setup mode, l'option à utiliser dans le menu UEFI peut donc être nommée de deux manières : soit elle parle de setup mode (ou quelque chose d'approchant), soit elle parle de supprimer la clé de plateforme (ou parfois l'ensemble des clés). Selon que l'UEFI offre seulement une option de suppression de l'ensemble des clés, ou des options séparées par type de clés, il peut être nécessaire d'effacer aussi les KEK et db avant d'ajouter les vôtres, en particulier si vous ne souhaitez pas conserver l'ensemble des clés préinstallées.
Une fois la PK supprimée, on peut installer nos clés. Il existe en général un menu qui permet de les importer depuis la partition EFI. Dès que nous aurons installé notre propre PK, le système repassera en mode de fonctionnement ordinaire, et l'installation des autres clés (KEK et db) pourrait, dans certains cas, ne plus être possible (même si, en principe, nos clés devraient être acceptées, puisqu'elles sont signées). Il est donc plus prudent d'installer la PK en dernier.
Le système devrait maintenant être en capacité de démarrer. Toutefois, la clé de déchiffrement du disque n'est pas encore enrollée dans le TPM, le déchiffrement va donc nécessiter la saisie de la phrase de passe LUKS.
Configuration du déchiffrement par le TPM au démarrage
Pour ne plus avoir à saisir la phrase de passe à chaque démarrage, nous allons enregistrer une seconde clé LUKS qui sera stockée dans le TPM. Le TPM possède plusieurs registres nommés PCR, pour Platform Configuration Registers, et numérotés de 0 à 23. Les valeurs contenues dans ces registres sont des hash sha1 et/ou sha256 de différents paramètres mesurés au démarrage comme le code exécutable de l'UEFI ou l'état secure-boot. La clé LUKS sera stockée dans le TPM sous une forme chiffrée par certains de ces registres, et ne pourra ainsi être restituée que si les registres PCR choisis ont les mêmes valeurs que lors de son enregistrement. L'utilisation de chacun de ces registres (pour certains cas l'usage est spécifique à Linux) peut être consulté sur le wiki d'Arch.
Nous utiliserons ici les PCR 0, 2 et 7. D'autres combinaisons sont possibles, par exemple en incluant les n°1 et 3, on verrouille non seulement la version de l'UEFI, mais aussi sa configuration. L'essentiel est de bien inclure le n°7 qui correspond à l'état secure-boot, ce qui inclus non seulement la liste des clés (PK, KEK et db) installées, mais également la clé particulière utilisée pour signer l'OS démarré. Puisque nous avons signé notre noyau avec notre propre clé privée, qui n'est partagée avec personne d'autre, nous avons donc la garantie que notre partition LUKS ne pourra être déchiffrée sans phrase de passe que lors du démarrage de notre UKI, signé par notre clé. En cas de démarrage, par exemple, depuis une image live publiée par une distribution, donc via shim, signé par la clé de Microsoft, la valeur du PCR n°7 sera différente, et le TPM ne fournira pas la clé LUKS. Les opérations qui suivent ne doivent donc être réalisées qu'après avoir réussi à démarrer sur l'UKI signée par notre clé secure-boot, sans quoi les valeurs des PCR utilisées ne seraient pas les bonnes.
Création de la clé et enregistrement dans le TPM
Pour commencer, nous allons créer une nouvelle clé que nous allons stocker dans un premier temps dans un fichier. Elle est convertie en notation hexadecimal car l'outil que nous utiliserons ensuite pour l'enregistrer dans le TPM n'accepte pas les données binaires. Cette clé doit rester secrète, on restreint donc immédiatement les droits au seul propriétaire :
dd if=/dev/random bs=64 count=1 | xxd -p -c64 | tr -d '\n' > /root/cle-luks
chmod 0400 /root/cle-luks
Nous ajoutons ensuite notre nouvelle clé à notre partition LUKS (cette commande demandera la saisie de la phrase de passe existante) :
cryptsetup luksAddKey --pbkdf-force-iterations 4 --pbkdf-memory 32 /dev/nvme0n1p2 /root/cle-luks
Les paramètres --pbkdf-* concernent la fonction dérivation de
clé : une phrase de passe
n'est souvent pas assez longue pour servir directement de clé, LUKS utilise donc un algorithme qui
permet de calculer, à partir de la phrase de passe, une clé suffisemment longue. Cette fonction est
paramètrable, pour pouvoir ajuster le bon compromis entre sécurité et rapidité, en fonction des
capacités du matériel. Ici nous avons créé une clé dans un fichier, avec une taille suffisante. On
peut donc configurer la fonction de dérivation avec une complexité minimale, pour gagner en rapidité
de déchiffrement.
On enregistre maintenant cette nouvelle clé dans le TPM avec la commande tpm2-initramfs-tool,
fournie par le paquet du même nom. C'est là que nous spécifions les numéros des registres PCR à
utiliser :
tpm2-initramfs-tool seal --data $(cat /root/cle-luks) --pcrs 0,2,7
Obtention de la clé pour déchiffrement au démarrage
La clé étant enregistré dans le TPM, nous allons pouvoir la récupérer pour déchiffrer le disque sans saisir la phrase de passe lors du démarrage. Cette récupération est susceptible d'échouer, par exemple en cas de mise à jour de l'UEFI, induisant un changement de valeur d'un des PCR ; il faut donc, en cas d'échec, que le système demande la phrase de passe, pour permettre de démarrer le système et de ré-enregistrer la clé avec les nouvelles valeurs, avec la commande ci-dessus. Ce processus devra se dérouler à l'exécution de l'UKI chargée depuis la partition EFI, nous allons donc placer tout ce qui est nécessaire dans l'initrd, qui est inclus dans l'UKI.
On commence par créer le script suivant, dans le fichier /etc/initramfs-tools/tpm2-cryptsetup :
#!/bin/sh
[ "$CRYPTTAB_TRIED" -lt "1" ] && exec tpm2-initramfs-tool unseal --pcrs 0,2,7
/usr/bin/askpass "Phrase de passe pour $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) : "
Et on le rend exécutable :
chmod +x /etc/initramfs-tools/tpm2-cryptsetup
Il faut maintenant faire en sorte que ce script, ainsi que les exécutables dont il a besoin, soient
inclus dans l'initrd. Nous allons pour cela créer un hook dans
/etc/initramfs-tools/hooks/tpm2-initramfs-tool :
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /usr/share/initramfs-tools/hook-functions
copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0
copy_exec /usr/bin/tpm2-initramfs-tool
copy_exec /usr/lib/cryptsetup/askpass /usr/bin
copy_exec /etc/initramfs-tools/tpm2-cryptsetup
copy_exec /usr/bin/sha256sum
Les cinq dernières lignes copient les fichiers nécessaires. Tout ce qui précède est un entête
nécessaire aux hooks initramfs, comme cela est indiqué dans la page de manuel d'initramfs-tools
(section Header). Ce script doit lui aussi être rendu exécutable :
chmod +x /etc/initramfs-tools/hooks/tpm2-initramfs-tool
Il nous reste à mettre à jour le fichier /etc/crypttab pour indiquer le script à utiliser pour le
déchiffrement (le seul changement par rapport à la version précédente est l'ajout de l'option
keyscript en fin de ligne) :
# <target name> <source device> <key file> <options>
nvme0n1p2_crypt /dev/nvme0n1p2 none luks,discard,keyscript=/etc/initramfs-tools/tpm2-cryptsetup
Vérification du système de fichier racine
Ce type d'installation est potentiellement vulnérable à une attaque, décrite dans cet article (en anglais), qui consiste à remplacer le volume LUKS par un volume similaire, contrôlé par l'attaquant et contenant un processus d'init malveillant qui pourra alors obtenir la clé depuis le TPM, puisqu'aucun PCR n'aura été altéré. Pour s'en prémunir, il faut s'assurer que le système de fichier déchiffré est bien le nôtre, avant de transférer le contrôle au processus d'init qu'il contient.
La solution présentée ici est basée sur ma propre compréhension de l'article cité ci-dessus, et elle est assez différente de ce qui y est suggéré (une solution qui authentifie la partition chiffrée à partir de sa clé, via le PRC 15 du TPM, et dont la mise en œuvre est basée sur l'utilisation de systemd dans l'initrd, ce qui n'est pas le cas ici).
N'ayant pris connaissance de ce problème qu'à la toute fin de la rédaction de ce guide, j'ai opté pour une solution qui changerait le moins possible ce qui était déjà rédigé (et déjà en place sur ma propre installation). Il s'agit donc d'une proposition personnelle, sans aucune garantie.
Pour vérifier l'intégrité du système de fichier, nous allons utiliser une somme de contrôle d'un fichier secret, présent uniquement dans notre volume chiffré, et stable dans le temps. La clé privée secure-boot spécifique à notre machine que nous avons déjà mise en place correspond à ces critères, c'est donc elle que nous allons vérifier avec un script écrit sur le modèle suivant :
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
attendu='<valeur de la somme de contrôle>'
mesure=$(echo "<une valeur aléatoire de salage>" \
| cat - $rootmnt/chemin/vers/la/clé/privée.key \
| sha256sum \
| cut -d ' ' -f 1 \
| tr -d '\n')
if [ "$mesure" != "$attendu" ]
then
panic "Système de fichier racine invalide"
fi
Les valeurs entre < et > sont bien sûr à adapter, ainsi que le chemin vers la clé privée. Le
salage permet d'éviter une attaque par collision sur la somme de contrôle, par exemple au moyen de
rainbow tables, cf. l'article
Wikipédia pour plus de détails. La valeur
attendu est à déterminée en utilisant la commande de mesure, une fois la valeur de salage fixée.
Attention, la fonction panic arrête le processus de démarrage. En cas d'erreur dans le script, on
risque donc de rendre le système impossible à démarrer. Il faudra alors démarrer sur un support live
et déchiffrer manuellement le disque (en saississant la phrase de passe) pour tout débloquer. Il est
donc plus prudent de remplacer, dans un premier temps, l'appel à panic par un simple affichage,
et de ne le rétablir qu'une fois qu'on s'est assuré que tout fonctionne bien.
Ce script est à placer dans le répertoire /etc/initramfs-tools/scripts/local-bottom/ (en le
rendant exécutable, comme d'habitude) ce qui permet son exécution après le montage du système de
fichier racine, mais avant l'exécution du processus d'init qui s'y trouve.
Il ne reste plus qu'à forcer la regénération de l'initrd :
update-initramfs -u -k all
Au redémarrage, la partition LUKS devrait désormais être déchiffrée automatiquement, sans avoir à saisir la phrase de passe.