dm-verity の実装

Android 4.4 以降では、オプションの device-mapper-verity(dm-verity)カーネル機能により確認付きブートをサポートしています。この機能では、ブロック デバイスの透明性の高い整合性チェックを実行できます。dm-verity の利用により、root 権限を保持してデバイスを不正使用する可能性がある永続的なルートキットを防ぐことができます。この機能により、Android デバイスの起動時に、そのデバイスが最後に使用されたときと同じ状態になります。

root 権限を持つ有害な可能性があるアプリ(PHA)は、検出プログラムから隠れたり、自分自身を偽装する場合があります。ルーティング ソフトウェアがこのような処理を実行できるのは、多くの場合検出ソフトウェアよりも多くの特権があり、検出プログラムに対して「嘘をつく」ことができるためです。

dm-verity 機能を使用すると、ファイル システムの下位のストレージ レイヤであるブロック デバイスを調べ、想定される構成と一致するかどうかを判断できます。これは、暗号論的ハッシュツリーを使用して行われます。各ブロック(通常は 4 KB)に SHA256 ハッシュがあります。

ハッシュ値はページのツリーに格納されるため、ツリーの残りの部分を検証するには、最上位の「ルート」ハッシュのみが信頼されている必要があります。ブロックのいずれかを改変するということは、暗号論的ハッシュを突破するのと同じことです。次の図はこの構造を説明したものです。

dm-verity-hash-table

図 1. dm-verity ハッシュ テーブル

公開鍵はブート パーティションに含まれています。これはデバイスのメーカーによって外部で確認される必要があります。この鍵は、ハッシュの署名を検証し、デバイスのシステム パーティションが保護され、変更されていないことを確認するために使用されます。

動作

dm-verity 保護はカーネル内で実行されます。カーネルが起動する前に root 権限取得ソフトウェアがシステムを不正使用すると、そのアクセスは保持されます。このリスクを抑えるため、ほとんどのメーカーは、デバイスに設定されたキーを使ってカーネルを検証します。このキーは、デバイスの出荷後は変更できません。

メーカーはこのキーを使用して第 1 レベルのブートローダーの署名を検証し、その後はそれより下のレベル、アプリのブートローダー、最終的にはカーネルの署名を検証します。メーカーが確認付きブートを利用する場合、カーネルの整合性を確認する方法がメーカーごとに必要です。カーネルが検証されている場合、カーネルはブロック デバイスを認識し、マウント時に検証します。

ブロック デバイスを検証する方法のひとつとして、内容を直接ハッシュして、保存された値と比較することがあげられます。ただし、ブロック デバイス全体を検証しようとすると、非常に時間がかかり、デバイスの電力も大幅に消費されます。デバイスの起動には非常に時間がかかるため、使用前に大幅に消耗することになります。

一方、dm-verity はブロックを個別に検証します。しかも、アクセスできたブロックに限られます。ブロックはメモリに読み込まれるとハッシュ化されます。ハッシュはツリーの上で検証されます。ブロックの読み取りはリソースの消費が激しいため、このブロックレベルの検証により発生する遅延は比較的小さくなります。

検証が失敗した場合、デバイスはブロックを読み取れないことを示す I/O エラーを生成します。このエラーは、ファイルシステムが破損している場合と同様に表示されます。

アプリは結果のデータなしで処理を続行する場合があります。たとえば、アプリの主な機能でこの結果が必要ない場合などです。ただし、データなしでアプリを続行できない場合は失敗します。

前方誤り訂正

Android 7.0 以降では、前方誤り訂正(FEC)により dm-verity がより強固になりました。AOSP 実装は一般的なリード・ソロモン誤り訂正符号で始まり、スペース オーバーヘッドを減らして破損ブロックのうち復元可能なブロックの数を増やすために、インターリーブと呼ばれる手法を適用します。FEC の詳細については、誤り訂正により厳密に適用された確認付きブートをご覧ください。

実装

概要

  1. ext4 システム イメージを生成します。
  2. そのイメージのハッシュツリーを生成します
  3. そのハッシュツリーの dm-verity テーブルを作成します
  4. dm-verity テーブルに署名してテーブル署名を生成します。
  5. テーブル署名と dm-verity テーブルを 1 つの信頼性メタデータにまとめます。
  6. システム イメージ、信頼性メタデータ、ハッシュツリーを連結します。

ハッシュツリーと dm-verity テーブルの詳細については、Chromium プロジェクト - 確認付きブートをご覧ください。

ハッシュツリーを生成する

冒頭で説明したように、ハッシュツリーは dm-verity に不可欠です。cryptsetup ツールはハッシュツリーを生成します。これと互換性のあるハッシュツリーは次のように定義されます。

<your block device name> <your block device name> <block size> <block size> <image size in blocks> <image size in blocks + 8> <root hash> <salt>

ハッシュを形成するために、システム イメージはレイヤ 0 で 4k のブロックに分割され、各ブロックに SHA256 ハッシュが割り当てられます。レイヤ 1 は、SHA256 ハッシュのみを 4k ブロックに結合することで形成され、より小さなイメージになります。レイヤ 2 は、レイヤ 1 の SHA256 ハッシュを使用して、同じように形成されます。

これは、前のレイヤの SHA256 ハッシュが 1 つのブロックに収まるまで行われます。ブロックの SHA256 を取得すると、ツリーのルートハッシュが得られます。

ハッシュツリーのサイズ(および対応するディスク使用容量)は、検証済みパーティションのサイズによって異なります。実際には、ハッシュツリーのサイズは 30 MB 未満の小さなものが多くなります。

レイヤ内に、前のレイヤのハッシュで完全に埋まっていないブロックがある場合は、0 で埋めて想定値の 4k にする必要があります。これにより、ハッシュツリーは削除されず、空白のデータが入った状態で完成します。

ハッシュツリーを生成するには、レイヤ 2 のハッシュをレイヤ 1 のハッシュに連結し、レイヤー 3 のハッシュをレイヤー 2 のハッシュに連結し、以下同様に処理を行います。このすべてをディスクに書き出します。これはルートハッシュのレイヤ 0 を参照しないことに注意してください。

要約すると、ハッシュツリーを構築する一般的なアルゴリズムは次のとおりです。

  1. ランダムなソルトを選択する(16 進エンコーディング)。
  2. システムイメージを 4k ブロックに分解する。
  3. ブロックごとに(ソルト化された)SHA256 ハッシュを取得する。
  4. これらのハッシュを連結してレベルを形成する。
  5. レベルを 4k のブロック境界まで 0 で埋める。
  6. レベルをハッシュツリーに連結する。
  7. 前のレベルを次のソースとして使用し、ステップ 2~6 を繰り返して、ハッシュが 1 つだけになるようにする。

それにより単一のハッシュが生成されます。これがルートハッシュです。これとソルトは、dm-verity マッピング テーブルの作成時に使用されます。

dm-verity マッピング テーブルを作成する

dm-verity マッピング テーブルを作成します。これは、カーネルのブロック デバイス(すなわちターゲット)とハッシュツリーの場所を指定します(これは同じ値になります)。このマッピングは fstab の生成と起動に使用します。また、このテーブルは、ブロックのサイズと、ハッシュツリーの開始位置である hash_start(具体的には画像の先頭からのブロック番号)も指定します。

信頼性ターゲット マッピング テーブルのフィールドの詳細については、cryptsetup を参照してください。

dm-verity テーブルに署名する

dm-verity テーブルに署名して、テーブル署名を生成します。パーティションを検証するとき、最初にテーブルの署名が検証されます。この処理は、固定の場所でブートイメージのキーに対して実行されます。キーは通常、メーカーのビルドシステムに含まれ、固定の場所のデバイスに自動的に組み込むことができます。

この署名とキーの組み合わせでパーティションを検証するには、次の手順に従います。

  1. /verity_key/boot パーティションに libmincrypt 互換の形式で RSA-2048 キーを追加します。ハッシュツリーを検証するために使用するキーの場所を特定します。
  2. 該当するエントリの fstab で、verifyfs_mgr フラグに追加します。

テーブル署名をメタデータにまとめる

テーブル署名と dm-verity テーブルを 1 つの信頼性メタデータにまとめます。メタデータのブロック全体にバージョンが設定されているため、拡張が可能です。たとえば、2 番目の種類の署名を追加したり、順序を変更したりできます。

サニティ チェックとして、マジック ナンバーがテーブルのメタデータの各セットに関連付けられていて、テーブルの識別に役立ちます。長さは ext4 システム イメージ ヘッダーに含まれているため、データ自体の内容を知らなくてもメタデータを検索できます。

これにより、未検証のパーティションを検証するよう選択していないことを確認できます。その場合、このマジック ナンバーがないと検証プロセスは停止します。この番号は 0xb001b001 のようになります。

16 進数でのバイト値は次のとおりです。

  • 1 バイト目 = b0
  • 2 バイト目 = 01
  • 3 バイト目 = b0
  • 4 バイト目 = 01

次の図に、信頼性メタデータの内訳を示します。

<magic number>|<version>|<signature>|<table length>|<table>|<padding>
\-------------------------------------------------------------------/
\----------------------------------------------------------/   |
                            |                                  |
                            |                                 32K
                       block content

次の表では、これらのメタデータ フィールドについて説明します。

表 1. 信頼性メタデータフィールド

項目 目的 サイズ
magic number(マジック ナンバー) fs_mgr がサニティ チェックとして使用する 4 バイト 0xb001b001
version(バージョン) メタデータ ブロックのバージョンの取得に使用 4 バイト 現時点では 0
signature(署名) パディングされた PKCS1.5 形式の表の署名 256 バイト
table length(テーブル長) dm-verity テーブルのバイト単位での長さ 4 バイト
table(テーブル) 前述の dm-verity テーブル テーブルの長さ(バイト数)
padding(パディング) この構造体は 0 で、長さが 32k にパディングされています。 0

dm-verity を最適化する

dm-verity のパフォーマンスを最大限に高める方法は次のとおりです。

  • カーネルで、ARMv7 用の NEON SHA-2 と ARMv8 用の SHA-2 拡張機能を有効にする。
  • 各種の先読みとプリフェッチ クラスタを実験して、デバイスに最適な設定を見つける。