VTPMを使ってTPMをお試ししてみる

# TPMとは

Trusted Platform Module とは セキュアな暗号プロセッサの国際規格です。
暗号鍵をデバイス内に持つことでセキュアな暗号プロセッサを実現しています。

TPMは通常ハードウェアとして実装されますが、ソフトウェアの実装もあります。TPMの要は暗号鍵がデバイスの中にあることなので、ソフトウェア実装にはあまり利点がなさそうですが、TPMの動作を検証する際には役に立ちます。

# なぜTPM?

社内でSPIREを調査・検証することがあったのですが、SPIREはAgentが動作しているマシンのアイデンティティを証明するためにいくつもの方法を用意しています。

[その中にはTPMを用いたもの](https://github.com/spiffe/spire/blob/main/doc/plugin_server_nodeattestor_tpm_devid.md)もあり、これを使ってみようかな？と思ったのですが、手軽にTPMを使う方法がよくわからなかったので、今回少し調べてみることにしました。

（この記事では、SPIREとTPMを組み合わせるところまでは行かずに力尽きていますが・・・）

# ソフトウェアTPMの実装

今回はswtpmを利用してみます。

https://github.com/stefanberger/swtpm

どうもSPIREからTPMを使っている実装では、`/dev/tpmX` にアクセスしているようで、まずはこれをどうやって作るか？ということを調べ始めました。

 https://github.com/stefanberger/swtpm/wiki/Using-the-IBM-TSS-with-swtpm に記載されている `Character device using tpm_vtpm_proxy`が、その方法のようでしたが、`tpm_vtpm_proxy` というカーネルモジュールが必要そうです。

名前にvtpmというのが出てきましたが、これが仮想TPMというやつのようです。おそらくソフトウェア実装のTPMをTPMとして見せるためのプロキシなのでしょう。

https://www.kernel.org/doc/Documentation/security/tpm/tpm_vtpm_proxy.rst

説明を読むと containerという言葉が出てくるが、今回は普通にホストから使いたいのだが、目的にあっているのだろうか？ ひとまず調査を進めました。

# VMで試してみる

OSはVagrantで作ったCentOS8です。
下記のようなVagrantファイルで作りました。

```
Vagrant.configure('2') do |config|
  config.vm.provider :virtualbox do |v|
    v.cpus = 1
    v.memory = 512
  end

  config.vm.box = "generic/centos8"
  config.vm.box_version = "3.2.20"

  config.vm.define "build-vm", autostart: false do |instance|
    instance.vm.hostname = "tpm-test"
    instance.vm.network :private_network, ip: "192.168.101.2" # IPアドレスは適宜変更してください
  end
end
```

さて、modprobeでロードできるかな？

```
$ modprobe tpm_vtpm_proxy
modprobe: FATAL: Module tpm_vtpm_proxy not found.
```

あえなく撃沈。


色々調べるとこれはどうやらカーネルビルド時に設定が必要らしい。

```
# cat /boot/config-4.18.0-240.22.1.el8_3.x86_64 |grep TPM
CONFIG_TCG_TPM=y
CONFIG_HW_RANDOM_TPM=y
# CONFIG_TCG_VTPM_PROXY is not set
```

CONFIG_TCG_VTPM_PROXYというのを設定する必要がありそうですが、このカーネルでは設定されていません。

# カーネルの再構築

いくつかのサイトを拾い読みしながら、カーネルを再構築しました。
（もっと良い方法があったら教えてください）

**参考サイト**

- https://qiita.com/tutuz/items/f824475d00b35dd39e6b
- https://tech.pjin.jp/blog/2015/11/14/linux-%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E3%81%AE%E5%86%8D%E6%A7%8B%E7%AF%89-%EF%BD%9E%E7%AC%AC1%E5%9B%9E%EF%BD%9E/



```
# 必要なパッケージのインストール
$ sudo yum install rpm-build ncurses kernel-devel bison flex bc openssl-devel

# 現在のカーネルと同じバージョンのソースパッケージを取得
$ wget https://ftp.riken.jp/Linux/centos-vault/8.3.2011/BaseOS/Source/SPackages/kernel-4.18.0-240.22.1.el8_3.src.rpm

# 展開方法がよくわからなかったのでcpioに変換して取りだす。
$ rpm2cpio kernel-4.18.0-240.22.1.el8_3.src.rpm | cpio -id
231928 blocks

# 解凍する
$ xz -dc linux-4.18.0-240.22.1.el8_3.tar.xz | tar xvf -

$ cd linux-4.18.0-240.22.1.el8_3/

# 今のカーネルのconfigを持ってくる
$ cp /boot/config-`uname -r` ./.config

# お作法なのかな？
$ make oldconfig
  LEX     scripts/kconfig/zconf.lex.c
  HOSTCC  scripts/kconfig/zconf.tab.o
  HOSTLD  scripts/kconfig/conf
scripts/kconfig/conf  --oldconfig Kconfig
#
# configuration written to .config
#
```

カーネルのconfigを変更します。

```
$ make menuconfig
```

コンソールベースのUIから下記を変更します。

Device Dricvers ---> 
Character decices --->
TPM Hardware Support --->
VTPM Proxy Interface を[M]にする

Kernel hacking --->
[*] Kernel debuggingをオフにする

で、カーネルをビルドします。（すごく時間がかかります）

```
$ make
```

ビルドしたカーネルモジュールをインストールします。

```
$ sudo make modules_install
```

```
$ ls -lah /lib/modules
total 16K
drwxr-xr-x.  5 root root   85 Dec  3 05:01 .
dr-xr-xr-x. 31 root root 4.0K May 11  2021 ..
drwxr-xr-x.  3 root root 4.0K Dec  3 05:02 4.18.0
drwxr-xr-x.  7 root root 4.0K Dec  3 03:17 4.18.0-240.22.1.el8_3.x86_64
drwxr-xr-x.  6 root root 4.0K May 11  2021 4.18.0-240.el8.x86_64
```

4.18.0というのが今回できたもののようです。

```
$ find /lib/modules | grep vtpm
/lib/modules/4.18.0/kernel/drivers/char/tpm/tpm_vtpm_proxy.ko
```

お目当てのカーネルモジュールも存在しています。

次にビルドしたカーネルをインストールします。

```
$ sudo make install
sh ./arch/x86/boot/install.sh 4.18.0 arch/x86/boot/bzImage \
	System.map "/boot"
cp: cannot stat '/boot/bls.conf': No such file or directory
sed: can't read /boot/loader/entries/4cf3b9fc42e040ab80d5666fb3657a40-0-rescue.conf: No such file or directory
make[1]: *** [arch/x86/boot/Makefile:155: install] Error 2
make: *** [arch/x86/Makefile:324: install] Error 2
```

ややエラーが発生していますが、無視して進みます。

```
$ ls -lah /boot
total 311M
dr-xr-xr-x.  5 root root 4.0K Dec  3 05:08 .
dr-xr-xr-x. 17 root root  224 May 11  2021 ..
-rw-r--r--.  1 root root  173 Apr  8  2021 .vmlinuz-4.18.0-240.22.1.el8_3.x86_64.hmac
-rw-r--r--.  1 root root  166 Sep 25  2020 .vmlinuz-4.18.0-240.el8.x86_64.hmac
lrwxrwxrwx.  1 root root   23 Dec  3 05:06 System.map -> /boot/System.map-4.18.0
-rw-r--r--.  1 root root 3.9M Dec  3 05:06 System.map-4.18.0
-rw-------.  1 root root 3.9M Apr  8  2021 System.map-4.18.0-240.22.1.el8_3.x86_64
-rw-------.  1 root root 3.9M Sep 25  2020 System.map-4.18.0-240.el8.x86_64
-rw-r--r--.  1 root root 186K Apr  8  2021 config-4.18.0-240.22.1.el8_3.x86_64
-rw-r--r--.  1 root root 186K Sep 25  2020 config-4.18.0-240.el8.x86_64
drwxr-xr-x.  3 root root   17 May 11  2021 efi
drwx------.  4 root root   83 Dec  3 03:20 grub2
-rw-------.  1 root root  76M Dec  3 05:08 initramfs-0-rescue-4cf3b9fc42e040ab80d5666fb3657a40.img
-rw-------.  1 root root  70M May 11  2021 initramfs-0-rescue-c49b328af64d4d1796c972833f1a0e82.img
-rw-------.  1 root root  30M May 11  2021 initramfs-4.18.0-240.22.1.el8_3.x86_64.img
-rw-------.  1 root root  32M May 11  2021 initramfs-4.18.0-240.el8.x86_64.img
-rw-------.  1 root root  18M May 11  2021 initramfs-4.18.0-240.el8.x86_64kdump.img
-rw-------.  1 root root  31M Dec  3 05:07 initramfs-4.18.0.img
drwxr-xr-x.  3 root root   21 May 11  2021 loader
lrwxrwxrwx.  1 root root   20 Dec  3 05:06 vmlinuz -> /boot/vmlinuz-4.18.0
-rw-r--r--.  1 root root 7.6M Dec  3 05:07 vmlinuz-0-rescue-4cf3b9fc42e040ab80d5666fb3657a40
-rwxr-xr-x.  1 root root 9.1M May 11  2021 vmlinuz-0-rescue-c49b328af64d4d1796c972833f1a0e82
-rw-r--r--.  1 root root 7.6M Dec  3 05:06 vmlinuz-4.18.0
-rwxr-xr-x.  1 root root 9.1M Apr  8  2021 vmlinuz-4.18.0-240.22.1.el8_3.x86_64
-rwxr-xr-x.  1 root root 9.1M Sep 25  2020 vmlinuz-4.18.0-240.el8.x86_64
```

- initramfs-4.18.0.img
- vmlinuz-4.18.0

ができているように見えます。

ここまで来たらシステムを再起動します。

```
$ sudo reboot
```

新しいカーネルで起動しました。

```
$ sudo modprobe tpm_vtpm_proxy
```

やっとこのコマンドが成功するようになりました。

# swtpmのインストール

やっとソフトウェアTPMを動かす準備が整ったのでswtpmをビルドします。

```
# 必要なパッケージをインストール
$ sudo yum install libtasn1-devel expect socat python3-twisted fuse-devel glib2-devel gnutls-devel gnutls-utils gnutls json-glib-devel libtpms-devel libseccomp-devel

# swtpmのソースをダウンロード
$ curl -LO https://github.com/stefanberger/swtpm/archive/refs/tags/v0.7.0.tar.gz
$ tar xzf v0.7.0.tar.gz
$ cd swtpm-0.7.0/

# ビルド・インストール
$ ./autogen.sh --with-openssl --prefix=/usr
$ make
$ sudo make install
```

# swtpmを起動しvTPM経由でアクセスする

キャラクタデバイスとしてtpmを実行してみます。

```
# vtpmのカーネルモジュールのロード（すでにロードしている場合は不要です。）
$ sudo modprobe tpm_vtpm_proxy

# swtpmが利用する作業ディレクトリを作成
$ mkdir /tmp/mytpm

# キャラクタデバイスとしてswtpmを起動
$ sudo swtpm chardev --vtpm-proxy --tpmstate dir=/tmp/mvtpm --tpm2 --ctrl type=tcp,port=2322
New TPM device: /dev/tpm0 (major/minor = 10/224)
```

swtpmは動かし続けたいので、別の端末からVMにログインし、tpmを操作するためのCLIもインストールします。

```
$ sudo yum install tpm2-tools
```

NVRAMと呼ばれるTPMのストレージにアクセスして登録されている情報を見てみます。

```
$ sudo tpm2_getcap handles-persistent
$
```

おや？空っぽです。

# swtpmの初期設定を実施する

通常TPMは工場出荷時にいくつかの情報がここに書き込まれているはずです。

swtpmではこの作業はコマンドラインから実施することができます。
下記にやり方が書かれていたので試してみました。

https://github.com/stefanberger/swtpm/wiki/Certificates-created-by-swtpm_setup

一度swtpmを停止して下記を実行します。

このコマンドにより ルートCAを作り、それで署名した証明書がTPMに格納されます。

```
# 一度データを消してやり直す
$ sudo rm -rf /tmp/mytpm

# 改めてディレクトリを作成
$ sudo mkdir /tmp/mytpm
$ sudo chown tss:root /tmp/mytpm

# セットアップ
$ sudo swtpm_setup --tpmstate /tmp/mytpm --create-ek-cert --create-platform-cert --tpm2
```

再びswtpmを起動します

```
$ sudo swtpm chardev --vtpm-proxy --tpmstate dir=/tmp/mvtpm --tpm2 --ctrl type=tcp,port=2322
New TPM device: /dev/tpm0 (major/minor = 10/224)
```

別端末からVMにログインして、NVRAMを見てみます。

```
$ sudo tpm2_getcap handles-nv-index
- 0x1C00002
- 0x1C00016
- 0x1C08000
```

先ほどと違い3行出力されました。

マニュアルによると 0x1C00002がEK cert、0x1C08000が  Platform certificateとのこと。

# EK証明書が正しいものかどうか検証してみる

EK certというのがこのTPM固有の証明書のようです。

この証明書は先程のセットアップ時に生成されたルート証明書で署名されています。
実際にはこのルート証明書は、PCをセットアップしたメーカーが用意するものとなるはずです。

セットアップ時に生成されたルート証明書は `/var/lib/swtpm-localca` に格納されています。

これを使ってEK証明書が正しいものかどうかを検証してみます。

EK証明書をNVRAMから取り出します。

```
$ sudo tpm2_nvread 0x1c00002 > ekcert.der
```

derファイルから証明書をpem形式で取り出します。

```
$ openssl x509 -inform der -in ekcert.der -outform pem -out ekcert.pem
```

swtpm_setupの際に生成されたルートCA証明書、中間証明書を連結する。
（これらは /var/lib/swtpm-localca に置かれている）

```
cat swtpm-localca-rootca-cert.pem issuercert.pem > bundle.pem
```

EK証明書を検証してみます。

```
$ openssl verify -CAfile bundle.pem ekcert.pem
ekcert.pem: OK
```

確かにセットアップ時に作られたCA証明書で署名されたものであることが確認できました。

# 終わりに

まだまだ調査は始まったばかり、というところですが、ひとまずここまで記事にしてみました。

TPMやカーネル再構築など、普段やらない操作ばかりなので、このドキュメント自体に誤りが含まれている可能性が多分にあります。もしお気づきの点がありましたら、お気軽にコメントください。

(さらにいうと https://github.com/spiffe/spire/blob/main/doc/plugin_server_nodeattestor_tpm_devid.md を試した方がいれば、その方法を教えて欲しいです・・）