LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Raspberry Pi 4 Model B で fs-verity と dm-verity のオーバーヘッドを計測する

はじめに

Raspberry Pi 4 Model B (Raspberry Pi 4) は安価に手に入るシングルボードコンピュータとして広く利用されている。
Raspberry Pi は、DebianベースOSのRaspberry Pi OSが搭載されている。 そのため、組込みLinuxの動作確認用のボードとして手軽に使うことができる。

一方で、fs-verityはファイルの改ざんを検出できるLinux Kernel 5.4の新機能である。
残念ながらfs-verityは、Raspberry Pi OSのデフォルトの設定では無効になっている。

そこで、本記事ではRaspberry Piカーネルの自前ビルドしたLinux Kernel 5.10でfs-verityのオーバーヘッドを測定した。

変更履歴

  • 2021/3/1: 記事公開

fs-verity とは

fs-verityとは、ファイルシステムと連携してファイルの実データの改ざんを検知することができるLinux Kernelの機能の一つである。
Linux Kernel 5.10では、ext4ファイルシステムとf2fsファイルシステムで上記がサポートされている。

fs-verityでは、ファイルの実データを一定サイズ (下記の例では4096バイト)毎に区切り、それぞれのハッシュ値を計算する。
計算したハッシュ値は、一定サイズになるまで同様にハッシュ値を計算していく。
このように算出されたハッシュツリーをあらかじめファイルの一部として保存し、実データとの比較によりデータの改ざんを検知することができる。

f:id:LeavaTail:20201228234304p:plain
fs-verityによるファイルの改ざんチェック

ただし、fs-verityを有効にしたファイルはRead-Onlyとなるので注意が必要である。

fs-verityの実装は、ファイルシステムreadpages操作をフックするようになっている。 fs-verityを有効にしたreadpages操作では、実データに加えてハッシュツリーもページキャッシュに読み込まれる仕様となっている。 そのため、fs-verityではDirect I/Oがサポートされていない。

fs-verityの使い方については、カーネルドキュメントやfs-verityのユーティリティ(fsverity-utils)のREADMEに記載されている。

git.kernel.org

ただし、上記のやり方のみだと、実データの保証に用いたハッシュツリーの改ざんを防ぐことができない。
そこで、fs-verityでは「署名ファイル検証」をサポートしている。

署名ファイル検証では、あらかじめ作成した証明書と秘密鍵からシグネチャを生成し、ファイルに付与する。
ファイルの読み込み時にこれらを比較することで、ハッシュツリーが書き換えられていないことを検証することができる

f:id:LeavaTail:20210228113342p:plain
署名付きfs-verity

署名ファイル検証で使用する証明書は、カーネルのkeyring(.fs-verity)を利用して実現する。
fs-verityでは PKCS#7のDER形式のX.509証明書を利用する。

dm-verity とは

fs-verity と似た機能として dm-verity がある。
dm-verityとは、device-mapperと連携してブロックデバイスの改ざんを検知することができるLinux Kernelの機能の一つである。

  • dm-verity: device-mapperの機能を利用して、ブロックデバイスの改ざんを検知できる機能
  • fs-verity: ファイルシステムの機能を利用して、ファイルの改ざんを検知できる機能

f:id:LeavaTail:20201231014348p:plain
dm-verityの概略図

dm-verityの使い方については、カーネルドキュメントやdm-verityのユーティリティ(cryptsetup)のREADMEに記載されている。

gitlab.com

また、dm-verityでも同様に「ハッシュツリーの署名検証」をサポートしている。

ファイルの読み込み時にこれらを比較することで、ハッシュツリーが書き換えられていないことを検証することができる

f:id:LeavaTail:20210228113342p:plain
署名付きfs-verity

署名ファイル検証で使用する証明書は、カーネルのkeyring(.fs-verity)を利用して実現する。
fs-verityでは PKCS#7のDER形式のX.509証明書を利用する。

実行環境

計測対象のRaspberry Pi 4 Model Bの詳細は下記のとおりである。

名前 詳細
Hardware Raspberry Pi 4 Model B Rev 1.2 (4GB)
OS Raspbian GNU/Linux 10 (buster)
Kernel Linux version 5.10.3-v7l+
SDカード HDMCSDH16GCL10UIJP-WOA
USBメモリ SILICON POWER SP032GBUF3B02V1K
IPアドレス (eth0) 172.16.1.3
ユーザ名 pi
ホスト名 raspberry

本記事では、SDカードにブートに必要なイメージを格納し、USBメモリに作成したext4ファイルシステムで計測する。

f:id:LeavaTail:20201231133644p:plain
本記事におけるデバイスとその用途の関係図

カーネルは前回の記事で作成したものを利用する。

leavatail.hatenablog.com

実験

ここまでの作業により、自前ビルドしたカーネルが起動することを確認できる。
そこで、作成したカーネルとルートファイルシステムをSDカードに書き込み、fs-verityを使用してみる。

fsverity-utilsのREADMEに記載されている使用方法に沿ってfs-verityによる改ざんチェックを実験する。

fs-verityの準備

  1. USBメモリ上にext4ファイルシステムを作成し、テスト用のファイルfileを準備する

     pi@raspberrypi:~ $ sudo mkfs.ext4 -O verity /dev/sda1
     pi@raspberrypi:~ $ mount /dev/sda1 /mnt
     pi@raspberrypi:~ $ head -c 1000000 /dev/urandom > /mnt/file
    
  2. OpenSSLを用いて証明書と秘密鍵を生成する

     pi@raspberrypi:~ $ openssl req -newkey rsa:4096 -nodes -keyout key.pem -x509 -out cert.pem
    
  3. 証明書をPEM形式からDER形式に変換する

     pi@raspberrypi:~ $ openssl x509 -in cert.pem -out cert.der -outform der
    
  4. fs-verityのキーリングに証明書を追加する

    pi@raspberrypi:~ $ sudo keyctl padd asymmetric '' %keyring:.fs-verity < cert.der
    
  5. fs-verityのキーリングの修正を禁止する

     pi@raspberrypi:~ $ sudo keyctl restrict_keyring %keyring:.fs-verity
    
  6. fs-verity検証時に署名を要求する

     pi@raspberrypi:~ $ sudo sysctl fs.verity.require_signatures=1
    
  7. ハッシュ値を確認する

     pi@raspberrypi:~ $ sha256sum /mnt/file                                                                                                                                                                                                                                                                                                                                                                                       
     1bfbef29d8891d710007696a02d0ad56297ca46a94eda673c520f4e1fd4daf6d  /mnt/file
    
  8. 対象のファイルに署名する

     pi@raspberrypi:~ $ fsverity sign /mnt/file file.sig --key=key.pem --cert=cert.pem
     Signed file '/mnt/file' (sha256:fe35b9281d3711296bcd49f65a6b0c4c26a33b0e2967c40c365ac94d0a4186af)
    
  9. fs-verityを有効化する

     pi@raspberrypi:~ $ sudo fsverity enable /mnt/file --signature=file.sig
     pi@raspberrypi:~ $ ls -l /mnt/file
     -rw-r--r-- 1 pi pi    1000000 12月 29 13:49 file
    

dm-verityの準備

dm-verityでは、改ざんチェックの対象となるブロックデバイス(データデバイス)とチェックのためのブロックデバイス(ハッシュデバイス)の2つを用意する必要がある。

そこで、測定対象デバイス(USBメモリ) 上でパーティション作成した。
このとき、データデバイス/dev/sda1を10GB、ハッシュデバイス/dev/sda2を残りの領域に割り当てた。

下記の手順にて測定環境を構築した。

  1. USBメモリ上にext4ファイルシステムを作成し、テスト用にデータを埋めておく

     pi@raspberrypi:~ $ sudo mkfs.ext4 /dev/sda1
     pi@raspberrypi:~ $ mount /dev/sda1 /mnt
     pi@raspberrypi:~ $ sudo dd if=/dev/urandom of=/mnt/file bs=1M count=1K
    
  2. ハッシュデバイスの構築準備

     pi@raspberrypi:~ $ sudo veritysetup format /dev/sda1 /dev/sda2 
     VERITY header information for /dev/sda2
     UUID:                   551c1f12-7d4d-4cf6-94a5-c03f6e66271f
     Hash type:              1
     Data blocks:            2621440
     Data block size:        4096
     Hash block size:        4096
     Hash algorithm:         sha256
     Salt:                   010b3c6dbfe22ebf306c1eedaa96de0a05cde16f273a8f5212a3fadd6c57ff43
     Root hash:              06dbc0a80648219b5ae5a9ab229ecea616541c9a9db548bfa9282b076bdf6598
    
  3. device-mapper (/dev/mapper/test)を作成

     pi@raspberrypi:~ $ sudo veritysetup create test /dev/sda1 /dev/sda2 06dbc0a80648219b5ae5a9ab229ecea616541c9a9db548bfa9282b076bdf6598
     Signed file '/mnt/file' (sha256:fe35b9281d3711296bcd49f65a6b0c4c26a33b0e2967c40c365ac94d0a4186af)
     pi@raspberrypi:~ $ ls -la /dev/mapper/test
     lrwxrwxrwx 1 root root 7 12月 30 04:12 /dev/mapper/test -> ../dm-0
    

性能計測

ここでは、fs-verityを有効化していない場合と有効化した場合でread性能にどれだけ差がでるかを確認する。

ここで、測定時の環境を再掲する。

名前 詳細
Hardware Raspberry Pi 4 Model B Rev 1.2 (4GB)
OS Raspbian GNU/Linux 10 (buster)
Kernel Linux version 5.10.3-v7l+
OSインストール先 SDカード
測定対象デバイス USBメモリ

fioで使用したパラメータは下記のとおりである。

  • direct I/Oは使用しない
  • シーケンシャルリードとランダムリードの2つを計測する
  • ブロックサイズは4KB
  • 非同期IO
  • ファイルサイズ1MBと10GBの2つを計測する

毎回の計測時に下記のような作業を1回実施し、その結果を測定値とした。

  1. 測定対象デバイスext4ファイルシステムを作成する
  2. 1MBまたは10GBのファイルを作成する
  3. fioでread性能を計測する
  4. fioでrandread性能を計測する
  5. fs-verityを有効化する
  6. fioでread性能を計測する
  7. fioでrandread性能を計測する

また、fioの計測前に# echo 3 > /proc/sys/vm/drop_cachesを実行してキャッシュを落としておく。

fs-verityの計測

まずは、1MBのファイルに対して計測してみた。

計測に使用したコマンドを下記に示す。

    pi@raspberrypi:~/fsverity $ sudo fio --filename=/mnt/file --direct=0 --rw=read --bs=4K --ioengine=libaio --runtime=300 --time_based --name=read1 --readonly --size=1M 
    pi@raspberrypi:~/fsverity $ sudo fio --filename=/mnt/file --direct=0 --rw=randread --bs=4K --ioengine=libaio --runtime=300 --time_based --name=read1 --readonly --size=1M              

ここでは、fioコマンドの結果によって得られたread/randread性能のみ抽出する。 結果は下記の通りである。

fs-verityなし fs-verityあり 性能変化率
read性能 82.2MiB/s 43.9MiB/s -47%
randread性能 6906KiB/s 5805KiB/s -16%

幣著が実験した環境では、シーケンシャルリードの性能が47%程度、ランダムリードが16%程度落ちていることが分かった。

また、10GBのファイルの読み込み性能も同様に計測してみた。

計測に使用したコマンドを下記に示す。

    pi@raspberrypi:~/fsverity $ sudo fio --filename=/mnt/file --direct=0 --rw=read --bs=4K --ioengine=libaio --runtime=300 --time_based --name=read1 --readonly --size=10G 
    pi@raspberrypi:~/fsverity $ sudo fio --filename=/mnt/file --direct=0 --rw=randread --bs=4K --ioengine=libaio --runtime=300 --time_based --name=read1 --readonly --size=10G              

ここでは、fioコマンドの結果によって得られたread/randread性能のみ抽出する。 その結果が下記の通りとなった。

fs-verityなし fs-verityあり 性能変化率
read性能 70.5MiB/s 27.9MiB/s -60%
randread性能 3792KiB/s 3340KiB/s -12%

幣著が実験した環境では、シーケンシャルリードの性能が60%程度、ランダムリードが12%程度落ちていることが分かった。

dm-verityの計測

これらを利用して、fs-verityと同様の環境で、dm-verityの性能測定をしてみた。

  • direct I/Oは使用しない
  • シーケンシャルリードとランダムリードの2つを計測する
  • ブロックサイズは4KB
  • 非同期IO
  • 測定対象デバイスファイルは/dev/sda1(dm-verityなし)と/dev/mapper/test (dm-verityあり)の2つを計測する
  • 測定対象デバイスファイルに構築されたext4ファイルシステム上の1MBファイルを計測する
dm-verityなし dm-verityあり 性能変化率
read性能 64.9MiB/s 43.8MiB/s -33%
randread性能 6582KiB/s 5420KiB/s -18%

幣著が実験した環境では、シーケンシャルリードの性能が33%程度、ランダムリードが18%程度落ちていることが分かった。

おわりに

本記事ではRaspberry PiカーネルLinux Kernel 5.10を自前でビルドして、fs-verityを有効化した。
またfioを用いて、fs-verityの有無によるread性能を計測することで、fs-verityのオーバーヘッドを測定した。

その結果、fs-verityを有効化したことでシーケンシャルリードが50%、ランダムリードが15%ほど性能低下していることが確認できた。
シーケンシャルリードの性能が大きく変化していることに関しては調査中であるが、readaheadとの相性が悪いのではないかと考えられる。

付録: fs-verity有効化でファイルを更新してみる

  • ファイルシステムから該当ファイルの実データを更新する: 実データの更新不可

     pi@raspberrypi:~ $ pi@raspberrypi:/mnt $ echo "FIX" >> file
     -bash: file: 許可されていない操作です
    
  • ファイルシステムから該当ファイルのメタデータを更新する: メタデータの更新可能

     pi@raspberrypi:~ $ sudo chmod 777 /mnt/file 
     pi@raspberrypi:~ $ ls -l /mnt/file
     -rwxrwxrwx 1 pi pi 1048576 12月 30 02:31 /mnt/file
    
  • ブロックデバイスファイルから該当データを更新する: 読み込み時にエラー

     pi@raspberrypi:~ $ umount /mnt
     # fileの実データが格納されているオフセットを直接書き換え
     pi@raspberrypi:~ $ sudo dd if=/dev/zero of=/dev/sda1 seek=268435456 bs=1 count=2
     pi@raspberrypi:~ $ sudo dd if=/dev/zero of=/dev/sda1 seek=270532608 bs=1 count=2
     pi@raspberrypi:~ $ mount /dev/sda1 /mnt
     pi@raspberrypi:~ $ cat /mnt/file
     cat: /mnt/file: 入力/出力エラーです
    

付録: dm-verityで署名機能を利用してみる

dm-verityの構築手順は下記を参考に、ルートハッシュの署名機能を有効にしてみた。

[http://kernsec.org/pipermail/linux-security-module-archive/2019-July/015347.html:embed:cite]

  1. 署名用の鍵と証明書を作成する

     leava@server:~/linux $openssl req -x509 -newkey rsa:1024 -keyout ca_key.pem -out ca.pem -nodes -days 365 -set_serial 01 -subj /CN=example.com
    
  2. コンフィグを修正する (CONFIG_SYSTEM_TRUSTED_KEYSのパスを指定)

     leava@server:~/linux $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig  
    
     -*- Cryptographic API  --->
        Certificates for signature checking  --->
          (ca.pem) Additional X.509 keys for default system keyring
    
  3. カーネルのビルド

     leava@server:~/linux $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage modules dtbs
    
  4. 作成した署名用の鍵をRaspberry Pi 4Bにコピーしておく

     leava@server:~/linux $ scp ca.pem pi@raspberry:~/
     leava@server:~/linux $ scp ca_key.pem pi@raspberry:~/
    
  5. 生成したカーネルイメージをSDカードに焼き、Raspberry Pi 4Bを起動する。

  6. 署名なしでルートハッシュの生成する

     pi@raspberrypi:~ $ veritysetup format /dev/sda1 /dev/sda2
    
  7. メールアーカイブに記載されているスクリプト(下記)を実行する(一部修正済み)

#!/bin/bash

NAME=test
DEV=/dev/sda1
DEV_HASH=/dev/sda2
ROOT_HASH=778fccab393842688c9af89cfd0c5cde69377cbe21ed439109ec856f2aa8a423    ★ここは修正する
SIGN=sign.txt
SIGN_NAME=verity:$NAME

# get unsigned device-mapper table using unpatched veritysetup
veritysetup open $DEV $NAME $DEV_HASH $ROOT_HASH
TABLE=$(dmsetup table $NAME)
veritysetup close $NAME

# sign root hash directly by CA cert
echo -n $ROOT_HASH | openssl smime -sign -nocerts -noattr -binary -inkey ca_key.pem -signer ca.pem -outform der -out $SIGN

# load signature to keyring
keyctl padd user $SIGN_NAME @u <$SIGN

# add device-mapper table, now with sighed root hash optional argument
dmsetup create -r $NAME --table "$TABLE 2 root_hash_sig_key_desc $SIGN_NAME"
dmsetup table $NAME

# cleanup
# dmsetup remove $NAME
# keyctl clear @u

参考