Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Arch LinuxでTPM2.0デバイスを使って暗号化したボリュームのパスフレーズ入力を省略する

以前の記事で、ルートファイルシステムを暗号化する手順を紹介しました。

blog.lufia.org

ルートとなるボリュームを暗号化すると、再起動のたびにパスフレーズの入力を促されますが、何度も入力するのは面倒です。そこで、TPMバイスに鍵を保存してパスフレーズの入力を省略しました。パスフレーズの入力が必須な状態と比べると少しだけセキュアではなくなりますが、個人的には許容できる範囲かなという印象です。

若干丁寧に説明を書いたので長い記事になってしまいましたが、証明書や鍵の管理を(直接は)していないので、やること自体はそこまで難しくないと思います*1

セキュアブートを有効にする

後で説明していますが、TPMバイスの保護機構では「セキュアブートの設定が変更されたときに保存している鍵を読み取れなく」できます。そのため、この手順でセキュアブートを有効にしていますが、不要なら飛ばしても影響はないと思います。

Arch Linuxでは、セキュアブートを実現する方法は以下の3通りあります。

  • PreLoader+HashTool
  • shim+MokManager
  • 自身の鍵で署名する

丁寧にやるなら、署名するためのマシンを分けて自身の鍵で署名するといいのだろうけど、そこまでするのは私物マシンの管理としては過剰に思いますし、同じマシンで署名するならそれほど意味があるとは思えないので、いちばん簡単なPreLoaderを使う方法を選びました。PreLoaderの方法では、悪意のあるブートローダカーネルに変更したあとでハッシュを追加すれば動いてしまうので、物理的にアクセスできる状況からは保護できませんが、少なくともリモートから書き換えられたとか、誤って不正なコードを実行した、などの状況は防ぐことができるので十分かなと思います。

セキュアブート自体の説明は以下の記事が分かりやすいですが、MOKにはあまり言及されていません。

PreLoader+HashToolの動作

まずはPreLoaderやHashToolが何を行うのか説明した方が理解しやすいと思うので書きます。PreLoaderはloader.efiを実行するだけのプログラムです。PreLoaderにはMicrosoftの署名が施されているので、セキュアブートが有効な状態でも問題なく実行できます。HashToolはPreLoaderから実行されるプログラムで、選択したファイルのハッシュをMOK(Machine Owner Key)へ追加するために使います。

従って、PreLoaderでセキュアブートを有効にすると、最終的にUEFIファームウェアは以下の順番でカーネルまで到達します。

  1. 電源を入れる
  2. UEFIファームウェアがPreLoaderを実行する
  3. PreLoaderはsystemd-bootのブートローダ(名前はloader.efiで固定)を実行する
  4. カーネルを実行する

このとき、UEFIファームウェア

などを確認します。検証に失敗した場合は実行エラーとなって、PreLoaderはHashToolへ制御を移します。HashToolはMOKハッシュ値を登録することができるので、ブートローダカーネルのハッシュをMOKへ登録して再度実行すると、今度は検証に成功するのでブートに成功します。

必要なプログラムのインストール

署名された状態のPreLoaderとHashToolが必要なのでAURからインストールします。また、ブートエントリに登録するためefibootmgrも必要です。

$ sudo pacman -S efibootmgr

$ git clone https://aur.archlinux.org/preloader-signed.git
$ cd preloader-signed
$ makepkg -si

これでPreLoaderとHashToolは/usr/share/preloader-signed以下にインストールされますが、このままではブート時に参照されないので、これを/boot/EFI以下へ手動でのコピーが必要です。また、PreLoaderloader.efiという名前のファイルを実行するので、systemd-bootのブートローダloader.efiとして参照できるようにします。

$ ls /usr/share/preloader-signed
HashTool.efi  PreLoader.efi

$ sudo cp /usr/share/preloader-signed/*.efi /boot/EFI/systemd/
$ sudo cp /boot/EFI/systemd/systemd-bootx64.efi /boot/EFI/systemd/loader.efi

上のコマンドではsystemd-bootx64.efiをコピーしていますが、これはPreLoader経由ではなくファームウェアが直接systemd-bootを実行するブートエントリも残すためです。セキュアブートに失敗してもすぐ戻せるように、残しておく方が安全です。

用意ができたらPreLoaderを実行するためのブートエントリを追加します。--partオプションはパーティション番号を指定するものです。

$ sudo efibootmgr --disk /dev/nvme0n1 --part 1 --create --label PreLoader --loader /EFI/systemd/PreLoader.efi

ここまでで、ブートエントリは以下のような状態になっているはず。

$ efibootmgr -v
BootCurrent: 0001
Timeout: 0 seconds
BootOrder: 0002,0001,0012,0000
Boot0000* Windows Boot Manager  HD(1,GPT,1880c8ba-98e7-4471-ba61-fcc1d165c58d,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)WINDOWS.......
Boot0001* Linux Boot Manager    HD(1,GPT,1880c8ba-98e7-4471-ba61-fcc1d165c58d,0x800,0x32000)/File(\EFI\systemd\systemd-bootx64.efi)
Boot0002* PreLoader HD(1,GPT,1880c8ba-98e7-4471-ba61-fcc1d165c58d,0x800,0x32000)/File(\EFI\systemd\PreLoader.efi)

セキュアブートを有効にする

PCを再起動します。このときUEFIのメニューでセキュアブートを有効にして、上記の手順で追加したPreLoaderブートエントリで起動しましょう。そうすると、この時点ではまだloader.efivmlinuz-linuxのハッシュが検証できないので、ブートの途中で検証エラーが発生して*2、HashToolが起動します。HashToolの画面では矢印キーを使って/bootパーティションのファイルを選択できるので、loader.efivmlinuz-linuxの2つを探して、ハッシュをMOKへ登録します。終わったらExitでHashToolを抜けて大丈夫です。

これで、セキュアブートを有効にした状態でブートできるのではないかと思います。セキュアブートが有効になっているかどうかは、以下のコマンドで確認できます。

$ sudo dmesg | grep 'Secure boot'
[    0.010526] Secure boot enabled
[    6.092896] Bluetooth: hci0: Secure boot is enabled

$ bootctl status
System:
     Firmware: UEFI 2.70 (Phoenix Technologies Ltd. 4660.22136)
  Secure Boot: enabled (user)
 TPM2 Support: yes
 Boot into FW: supported

なんらかの理由で起動に失敗した場合は、おそらくUEFIのメニューにブートオーダーの設定もあると思うので、ここから以前のブートエントリを優先に戻して起動すればよいです。

カーネルやsystemd-bootをアップデートした場合

systemdパッケージを更新しても、それだけではブートローダ/boot以下にコピーされません。都度bootctlcpを実行してもいいですが面倒なので、/etc/pacman.d/hooks/systemd-boot.hookなどに書いておくことをおすすめします。

[Trigger]
Type = Package
Operation = Upgrade
Target = systemd

[Action]
Description = Upgrading systemd-boot...
When = PostTransaction
Exec = /bin/sh -c 'bootctl update && cp "$(bootctl -p)"/EFI/systemd/{systemd-bootx64,loader}.efi'

また、vmlinuz-linuxloader.efiを変更した場合、ハッシュ値が変わるのでMOKでの検証が失敗します。このときは上記と同様にHashToolが起動するので、新しいファイルのハッシュを登録し直せば起動できるようになります。

気になりごと

Arch Linuxでは、カーネルは頻繁に更新されるので、HashToolで追加したハッシュの上限はあるのか、あるなら古いハッシュを削除する方法はあるのか、が気になっています。HashTool.cを読むと、MOKの実態はUEFIのMokList変数らしいので、これを扱う方法があればよいのですが、この値はブートサービス固有の変数で通常起動した後は見れません。少くとも/sys/firmware/efi/efivars以下では参照できませんでした。自由に書き換えられると困るのでそうですね...

それから、mokutilコマンドでdbをみたとき、dbに含まれているMicrosoftの証明書は期限が2026年でした。Windows環境ならファームウェアアップデートなどで対応するのかもしれませんが、Linuxではどうすればいいんだろう。

busybox initをsystemd initに変更する

Arch Linuxmkinitcpioは、デフォルトではbusyboxフックを使いますが、systemd-cryptenrollを使うためにはsystemdフックが必要なので切り替えます。難しく聞こえますが、やることは/etc/mkinitcpio.confHOOKS変数を変更するのと、この変更によってカーネルオプションの書式が少し変わるので対応するだけです。

まずはmkinitcpio.confの差分です。

--- mkinitcpio.conf.orig 2022-01-29 16:55:28.098086104 +0900
+++ mkinitcpio.conf   2022-01-31 01:24:39.829166470 +0900
@@ -49,7 +49,7 @@
 #
 ##   NOTE: If you have /usr on a separate partition, you MUST include the
 #    usr, fsck and shutdown hooks.
-HOOKS=(base udev autodetect modconf keyboard keymap block encrypt filesystems fsck)
+HOOKS=(base systemd autodetect modconf keyboard block sd-encrypt filesystems fsck)
 
 # COMPRESSION
 # Use this to compress the initramfs image. By default, zstd compression
@@ -64,4 +64,4 @@
 
 # COMPRESSION_OPTIONS
 # Additional options for the compressor
-#COMPRESSION_OPTIONS=()
+COMPRESSION_OPTIONS=(-9)

autodetectフックは省略可能ですが、このフックはinitramfs-linux.imgから不要なモジュールを削減してくれるものです。イメージは小さいほうが読み込み速度などで有利なので、基本的には含めるほうがいいでしょう。ただし、必要なモジュールなのに誤って削減される場合があるらしく、削減前のイメージをinitramfs-linux-fallback.imgとして生成します。

また、systemdフックはbusyboxフックに比べてファイルサイズが大きくなる傾向があるのでCOMPRESSION_OPTIONSも変更していますが、/bootパーティションに余裕があるなら設定しなくても問題ありません。

設定を更新したら、新しいinitramfs-linux.imgを生成します。

$ sudo mkinitcpio -p linux

systemdフックへの変更に伴い、カーネルオプションの書式も変わります。

--- arch.conf.orig   2022-02-12 22:05:35.920657630 +0900
+++ arch.conf 2022-02-12 22:05:41.680559575 +0900
@@ -2,4 +2,4 @@
 linux /vmlinuz-linux
 initrd /intel-ucode.img
 initrd /initramfs-linux.img
-options cryptdevice=UUID=ef718fc5-8eb5-44e8-904b-34c3705d3390:cryptroot root=/dev/mapper/cryptroot rw
+options luks.name=ef718fc5-8eb5-44e8-904b-34c3705d3390=cryptroot root=/dev/mapper/cryptroot rw

上記のArchWikiを読む限り、rootオプションは無くても動きそうにみえますが、無くしてしまうとブート時に、

A start job is running for /dev/gpt-auto-root

タイムアウトしてしまってブートできませんでした。

TPM 2.0にLUKS2の鍵を入れる

TPMは、雑に表現すると「機密情報を安全に保存するためのストレージ」です。tssグループに参加していれば一般ユーザーでも利用できるので、例えばSSH秘密鍵を安全に保存するためにも利用できます。一般的なストレージとの大きな違いは、ハードウェア構成やブート順などに変化があれば、保存されている機密情報を無効化できるところかなと思います。

無効*3にする条件にはPCR(Platform Configuration Register)の番号を与えます。PCRは0から開始する連番になっていて、ハードウェアによって上限は異なります*4が、0から7くらいまでの用途は事前に決められています。たとえばPCR0には「ファームウェアのコードに変更があれば変わる」ハッシュ値が保存されていて、PCR4はブートローダに変化があれば変わるハッシュ値です。この値は、

PCR[n] = sha256(PCR[n] + data)

のように計算されるので、一度でも変更されると、たとえ設定を元に戻しても以前の値には戻りません。well-knownな各PCRの値は以下のマニュアルにある表がわかりやすいかなと思います。

それぞれのPCRは独立して計算されますが、ブート順を変更すると一緒にパーティションテーブルの状態が変わるなどはよくあるので、その場合はどちらか片方だけ指定しておけば十分です。

PCRの変化例

具体的にPCRの変化を実験してみました。

  1. セキュアブートを無効に変更
  2. Archインストーラからブート

この場合、手元のハードウェアではPCR4、PCR5、PCR8が変化しました。

--- tpm2_pcrread 2022-01-30 03:10:30.473355620 +0900
+++ tpm2_pcrread  2022-01-30 03:09:52.159120902 +0900
@@ -7,2 +7,2 @@
-    4 : 0x510D823ADDB47C0F3AE033F820F5B139A5140D180BB71508B83686DC1873BC08
-    5 : 0xCEE0B028F90CDB9446AB335E2E7DB173908E1668AFDBA7E6C089E3911DE8059F
+    4 : 0x2BE06D1448029581DC41BFBC72F3E5F086F1104C2B16CA5F3F64A8F11FDD199F
+    5 : 0xB9344EFE635B503DB6474937C18839770460B3A4365916D07D13B00FD3D63AD6
@@ -11 +11 @@
-    8 : 0xE0C4DFC3BBDFBD8B79583B9FC5EC83912A2C86977D5FCC110C51C29D6EC846CF
+    8 : 0x3723C256E87505FC1174CCA1F71BC8145D8703D46AE88C88537DF9AF72E2AB0B

次に

  1. セキュアブートを有効に戻す
  2. カーネルを更新

したときの様子です。PCRの計算には現在のハッシュ値も含まれているので、元の値には戻りません。

--- tpm2_pcrread 2022-01-30 03:09:52.159120902 +0900
+++ tpm2_pcrread  2022-01-30 03:32:01.209482235 +0900
@@ -7 +7 @@
-    4 : 0x2BE06D1448029581DC41BFBC72F3E5F086F1104C2B16CA5F3F64A8F11FDD199F
+    4 : 0xAB040291BAFDE1BD13A36FA243E9CECAED92266C29EC1FBAACC99A6AACE51E3E
@@ -10,2 +10,2 @@
-    7 : 0xF6EC92324515892DBA62227C794696324EA38639C60EF94F138B59C0270B0C8A
-    8 : 0x3723C256E87505FC1174CCA1F71BC8145D8703D46AE88C88537DF9AF72E2AB0B
+    7 : 0xE622A16146FB45989BFEAC7FE32B961A933269E78F111B7DB61CE1B37F54BA40
+    8 : 0x9D64BE0CC068CED5CD0ACA32C101B4270B75B61DA74293245E1394C39B62FCA9

TPM2にLUKS2の鍵を入れる

ようやく本題です。LUKS2でボリュームを暗号化している場合、ブート時にパスフレーズの入力が必要です。ところでLUKS2はパスフレーズとは別の鍵をキースロットに複数登録することができ、それを使った複合が可能です。この鍵をTPM2に入れておき、ブート時に参照することでパスフレーズの入力を省略します。

まずはTPMが利用可能かどうかを調べます。調査などに便利なのでtpm2-toolsパッケージもインストールしておきます。

$ journalctl -k --grep=tpm

$ sudo pacman -S tpm2-tools
$ sudo tpm2_getrandom 20
$ sudo tpm2_getcap pcrs
$ sudo tpm2_pcrread sha256

TPM2を一般ユーザーでも利用したい場合はtssグループに参加させると使えます。

$ sudo homectl update --member-of=wheel,input,tss lufia

(いちどログアウトしてから)

$ tpm2_getrandom 20

systemd-cryptenroll

TPM2とLUKSキースロットの管理はsystemd-cryptenrollで行います。同様のことを行うプログラムにclevisもありますが、ここではsystemd-cryptenrollを選びました。この記事では言及しませんが、systemd-cryptenrollはTPM2に限らず、FIDO2を使った複合などにも対応しています。

systemd-cryptenrollの使い方ですが、ここまで進めているならそれほど難しくはなく、決まった順番にコマンドを実行するだけです。まずはTPM2デバイスの名前を調べます。

$ systemd-cryptenroll --tpm2-device=list
PATH        DEVICE     DRIVER
/dev/tpmrm0 IFX0785:00 tpm_tis

ここでデバイスの名前は/dev/tpmrm0と分かりました。上記の出力例では、TPMバイスは1つしかないのでautoとすることで省略可能です。あとはsystemd-cryptenrollコマンドに暗号化したディスクを与えましょう。

$ sudo systemd-cryptenroll /dev/nvme0n1p2 --tpm2-device=auto --tpm2-pcrs=4+7

ブート時にTPM2へアクセスするため、/etc/mkinitcpio.confMODULE変数にTPM2ドライバを追加します。

--- mkinitcpio.conf.orig 2022-01-29 16:55:28.098086104 +0900
+++ mkinitcpio.conf   2022-01-31 01:24:39.829166470 +0900
@@ -4,7 +4,7 @@
 # run.  Advanced users may wish to specify all system modules
 # in this array.  For instance:
 #     MODULES=(piix ide_disk reiserfs)
-MODULES=()
+MODULES=(tpm_tis)
 
 # BINARIES
 # This setting includes any additional binaries a given user may

あとはinitramfs-linux.imgをビルドして終わりです。

$ sudo mkinitcpio -p linux

これで次回以降のパスフレーズ入力が省略できます。

PCRハッシュ値に変化があった場合

上記ではPCR4とPCR7どちらかに変化があったとき鍵を無効にする設定でコマンドを実行しました。そのため、例えばカーネルが更新されたときはセキュアブートによってMOKも更新されることになり、PCRの値も変化して鍵が無効になります。この場合はTPM2から鍵を取り出せなくなるため、パスフレーズの入力が必要です。

パスフレーズでブートした後、以下のコマンドによって再びパスフレーズを省略できます。LUKS2のキースロットは有限ですし、TPM2から古い鍵を取り出す手段は失われているので、--wipe-slot=tpm2オプションを忘れないようにしましょう。

$ sudo systemd-cryptenroll /dev/nvme0n1p2 --wipe-slot=tpm2 --tpm2-device=auto --tpm2-pcrs=4+7

参考リンク

*1:少なくとも他の方法に比べたら...

*2:赤くエラーが表示されるので少し驚くかも

*3:sealと表現される

*4:tpm2_pcrreadコマンドで確認できる