Blog

2020.03.13

TEE/OP-TEEの紹介

エンジニアの森田です。

本記事ではIoT機器のセキュリティを考える上で重要なTrusted Execution Environment(TEE)標準について説明し、 その実装であるところのOpen Portable Trusted Execution Environment(OP-TEE)を動作させてみます。 また、OP-TEE上で動作するTrusted Application(TA)の作成方法やアプリケーションからの呼出し方法について解説し、改造して動作を確かめてみます。

はじめに

Trusted Execution Environment(TEE)とはプロセッサ上に隔離された実行環境を用意する事でセキュリティを高める技術であり、 そのTEEに関する標準がGlobalPlatformによって策定されています。

GlobalPlatformは伝統的にはSecure Element(SE)というセキュアなデバイスに関する規格を策定している団体であり、 SEはクレジットカードやSIMカードといったICカード上に搭載される専用チップとして利用されてきました。 それに対し、TEEはARMのような汎用のプロセッサ上におけるセキュアな実行環境を提供するハードウェアとソフトウェアも含めた標準であり、スマートフォン上でのセキュアな生体認証や、セキュアなバンキングサービスを実現する技術として注目されています。 GlobalPlatformはSEと合わせたTEEのユースケースとして以下の例を挙げています。

  • コネクテッドカー
  • リモートSIMプロビジョニング
  • Industry 4.0
  • コンテンツ保護/DRM
  • 交通チケット
  • ウェアラブルデバイス
  • モバイル認証
  • 生体認証

(出典: https://globalplatform.org/use-case/)

Linux、Android、iOSといった我々が普段よく目にするOS(Rich OS)が動作する環境はRich Execution Environment(REE)と呼ばれます。 TEEはREEから隔離されたプロセッサ上のセキュアな領域の事を指し、機密データの保護やコードの安全な実行環境を提供します。

TEE環境で扱う資産をREEから保護するためのハードウェア機能が要件として定められており(TEE System Architecture v1.2, 2.2.1 TEE High Level Security Requirements)、 これをサポートするためのハードウェア機能として以下のようなものが挙げられます。

  • ARM TrustZone
  • Intel SGX
  • RISC-V Keystone

(参考: 須崎有康, 塚本明, 小島一元, 中島健太, Hoang Trong Thuc, 師尾彬, “3種類のTEE比較(Intel SGX, ARM TrustZone, RISC-V Keystone)”)

また、TEE上で動作するOS(Trusted OS)についても幾つかの実装があります。

(出典: 須崎有康, “TEEを中⼼とするCPUセキュリティ 機能の動向 (RISC-V, ARM, etc)”, p.20, 原文中のURL部分はリンク化しました)

また、TEE上で動作するアプリケーションはTrusted Application(TA)と呼ばれ、セキュアな処理を実行します。 REE上で動作するアプリケーション(TEE Client)に対するインターフェースが TEE Client API Specification Version 1.0 に定められており、 REE上のアプリケーションはこれを利用してセキュアな処理をTAに移譲できます。

今回はARM TrustZoneと、Trusted OSのオープンソースソフトウェア実装であるOpen Portable trusted Execution Environment(OP-TEE)の組み合わせでTEE上でアプリケーションがどのように動くのか、どのように作られているかを紹介します。

また、ARM TrustZoneの文脈ではREE、TEEの事をNormal World、Secure Worldと呼ばれるようです。 以下、Normal WorldというとLinuxや通常のアプリケーションが動作する環境、 Secure WorldというとOP-TEEや、TAが動作する環境と考えてください。

デバイスの選定

ではOP-TEEのドキュメントに沿って、実際にOP-TEEを動かしてみましょう。

まず、OP-TEEを動作させるデバイスを検討しましょう。 ドキュメントを見ますと、多くの市販のARMマイコンボードがサポートされており、また、 さまざまな環境におけるビルド、実行の方法が記述されている事がわかります。

今回はお試しということで、手っ取り早い方法を採用します。

この方法ではQEMUを使ってARMv7-AコアをPC上でエミュレーションする形をとりますので、皆さんのお手元でも試せると思います。 (やる気のある方は是非物理デバイス上で動かしてみてください)。

ビルド環境、実行用のQEMUのホスト環境として Ubuntu 18.04.1 を想定しています。またストレージに40GB程度の空きが必要です。 また、公式の手順にapt-getを利用しているところがあります。 筆者はFedora 31上の仮想環境上のUbuntu 18.04.1 Desktop版にて、 OP-TEE Trusted OS version 3.8.0 (dcf64f87d66e49e75330917c4f143dc9fba6eb3b)を動作させました。

事前準備

ひとまず関連パッケージを最新にしておきます。

sudo apt-get upgrade

次にドキュメントの通り、Prerequisitesとして要求されているソフトウェアをインストールします。

sudo apt-get install android-tools-adb android-tools-fastboot autoconf \
        automake bc bison build-essential ccache cscope curl device-tree-compiler \
        expect flex ftp-upload gdisk iasl libattr1-dev libc6:i386 libcap-dev \
        libfdt-dev libftdi-dev libglib2.0-dev libhidapi-dev libncurses5-dev \
        libpixman-1-dev libssl-dev libstdc++6:i386 libtool libz1:i386 make \
        mtools netcat python-crypto python3-crypto python-pyelftools \
        python3-pycryptodome python3-pyelftools python-serial python3-serial \
        rsync unzip uuid-dev xdg-utils xterm xz-utils zlib1g-dev

続いてOP-TEEのビルドディレクトリを作成します。 続いてOP-TEE gitレポジトリを取得するのですが、その前にrepoというコマンドをインストールしておきます。

sudo apt-get install repo

repoコマンドはgitのユーザ設定を要求してきます。 未設定の場合は予め設定しておきます。

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

リポジトリの取得

それではrepoコマンドを使ってOP-TEE関連のレポジトリを取得していきます。

mkdir optee-qemu && cd optee-qemu
repo init -u https://github.com/OP-TEE/manifest.git

最終的に以下のメッセージが表示されたら成功です。

repo has been initialized in /home/user/optee-qemu

今回はデフォルトの設定であるところのQEMUビルドだったためrepo initコマンドに対するオプションは最小限でしたが、 他の実行環境向けにビルドする場合や、特定のバージョンを利用したい場合、オプションにて指定する必要があります

次に依存するプロジェクトのリポジトリを取得します。 ネットワークの状況など環境条件も影響すると思いますが、完了までに十数分程かかるかもしれません。

repo sync

コマンドが無事に終了すると下記メッセージが表示されるはずです。

repo sync has finished successfully.

ビルドと実行

次はビルドですが、まずはクロスコンパイラツールチェインを準備します。

cd build
make toolchains -j2

本記事執筆時は以下のように二つのツールがダウンロードされました。

Downloading gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf ...
Downloading gcc-arm-8.3-2019.03-x86_64-aarch64-linux-gnu ...

https://developer.arm.com からバイナリをダウンロードしているようですが、 筆者の環境では20分ほどがかかりました。 また、make toolchains -j2を途中で中断してしまった場合、 ~/optee-qemu/toolchains に中間結果が残ってしまい、 再実行が上手くいかないようでした。 make toolchains -j2 を中断→再実行したい場合は ~/optee-qemu/toolchains ディレクトリを削除してください。

次に、以下のコマンドを打つとビルドと実行が始まります。 筆者の環境ではビルドが終るまでに40分ほどかかりました。

make run

最後までビルドが通ると、 以下のようなメッセージ表示とともに、ターミナルアプリケーションに新しいタブが二つ追加されます。

* QEMU is now waiting to start the execution
* Start execution with either a 'c' followed by <enter> in the QEMU console or
* attach a debugger and continue from there.
*
* To run OP-TEE tests, use the xtest command in the 'Normal World' terminal
* Enter 'xtest -h' for help.

# Option “-x” is deprecated and might be removed in a later version of gnome-terminal.
# Use “-- ” to terminate the options and put the command line to execute after it.
# Option “-x” is deprecated and might be removed in a later version of gnome-terminal.
# Use “-- ” to terminate the options and put the command line to execute after it.
cd /home/user/optee-qemu/build/../out/bin && /home/user/optee-qemu/build/../qemu/arm-softmmu/qemu-system-arm \
	-nographic \
	-serial tcp:localhost:54320 -serial tcp:localhost:54321 \
	-smp 2 \
	-s -S -machine virt,secure=on -cpu cortex-a15 \
	-d unimp -semihosting-config enable,target=native \
	-m 1057 \
	-bios bl1.bin \
	-object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000 -netdev user,id=vmnic -device virtio-net-device,netdev=vmnic
QEMU 3.0.93 monitor - type 'help' for more information

この状態でQEMU上の仮想デバイスは停止している状態ですので、qemuのgdb上にc (continue)を打ち込んで起動します。

(qemu) c

このQEMU上のARM仮想デバイスにはシリアル入出力が二本用意されており、それぞれ、Secure World(=TEE)上で動くOP-TEE OS、Normal Woald (=REE)上で動くLinuxの入出力として使われます。 make runの最後で新しく開いた2つのターミナルタブはこの2つのシリアルに対応しています。

今回はターゲットがQEMUだったため、make run でビルドから実行まで一度に進められました。 もし物理デバイス上でOP-TEEを動かす場合は、ビルド後にboot imageをFlash ROMに焼く、シリアル通信のためのケーブルを配線する、等もう少し手間が必要になると思います。

さて、Normal World側のターミナル上ではU-BootLinuxのブートが完了すると以下のようなログイン画面が表示されます。 ここはrootとタイプしてログインします。

Welcome to Buildroot, type root or test to login
buildroot login:

つづいて次のコマンドでop-teeのリグレッションテストを流します。

# xtest

そこそこ時間がかかりますが、基本的なTEE API等が動いていることが確認できます。

他にもサンプルアプリケーションが組み込まれているので動かしてみます。

# optee_example_hello_world 
Invoking TA to increment 42
TA incremented value to 43

hello_worldと言いつつ、Normal World側から見ると単純に1を足すだけの簡単なアプリのようです。

一通り動きを確認したらqemuのgdb画面に戻ってq(quit)を打ちこんで終了します。

(qemu) q

以降はこの簡単なアプリケーションoptee_example_hello_worldがどのように作られているかを詳しく見てみます。

ビルドシステム概観

前節でQEMU上でOP-TEEを含むシステムを仮想的なデバイス上で動かしましたが、 このためには以下を含むイメージを用意する必要があります。

  • Normal World用bootloader (U-boot)
  • Normal World用OS kernel (OP-TEE driver入りLinux)
  • 基本的な設定、linuxのコマンド、TEE Clientを含むrootfs
  • Secure World用Firmware (ARM Trusted Firmware)
  • Secure World用OS kernel (OP-TEE OS)
  • TA

最終的にはこれらが一つのイメージとなってデバイスに書き込まれますが、 今回はSecure Worldで動作するアプリケーションであるTAと、Normal Worldで動作するアプリケーションであるTEEP Clientに注目します。

さきほど実行したoptee_example_hello_world はBuildroot(組み込みデバイス向けのLinuxブートイメージ作成ツール)の外部カスタムパッケージとして登録され、rootfsに配置されているようです。 optee-qemu/build/br-extがBuildrootから読み込まれる外部ツリーになります。 さきほど実行したoptee_example_hello_worldが含まれる、 optee_examplesというパッケージを見てみます。

optee_example_hello_worldのソースツリーを見てみると、以下のようになっています。

optee_examples/hello_world/
├── Android.mk
├── CMakeLists.txt
├── host                            # TEE Client実装ディレクトリ
│   ├── main.c                      # TEE Clinet実装
│   └── Makefile
├── Makefile
└── ta                              # TA実装ディレクトリ
    ├── Android.mk
    ├── hello_world_ta.c            # TA実装
    ├── include
    │   └── hello_world_ta.h
    ├── Makefile                    # TAビルド必須ファイル (BINARY = uuid)
    ├── sub.mk                      # TAビルド必須ファイル (src-y += hello_world_ta.c)
    └── user_ta_header_defines.h    # TAビルド必須ファイル

まず大きくhost, taという二つのディレクトリに分かれていることがわかります。 hostディレクトリ以下はNormal Worldで動くTEE Clientの実装、taディレクトリにはSecure Worldで動くTAの実装が記述されています。

Host(TEEP Client)実装

host側の実装を見てみましょう。

optee_examples/hello_world/host/main.c の内容を見ると、 以下のTEE Client APIを順に呼び出しています。

  • TEEC_InitializeContext
  • TEEC_OpenSession
  • TEEC_InvokeCommand
  • TEEC_CloseSession
  • TEEC_FinalizeContext

これらのAPIはTEE Client API Specification Version 1.0に定義されており、 TAの機能を利用する際のNormal World側から見た基本的なインターフェースになっています。 これらのAPIを利用するためにteecというライブラリを利用していますが、その他はビルド方法含め、ごく通常のLinuxプログラムと同じです。

TA実装

TAはSecure Worldで動作するプログラムであり、通常のLinuxアプリケーションとは異なります。 こちらのドキュメントにあるように、 TAはNormal Worldで動作する通常のプログラムとは異なり、TA-devkitを利用してビルドします。 TA-devkitの作法に従うためにTAを作成するのに最低限必要なファイルがあります。

  • Makefile
  • sub.mk
  • user_ta_header_define.h

また、Makefileには必ず記述しなければならない内容があります。 BINARYという変数はTAのuuidを示し、TEECからTAを指定する際にこのuuidが必要になります。 またTA-devkitを利用するため、ta_dev_kit.mkというスクリプトをインクルードする必要があります。 hello_worldの例を見ると以下のような記述がある事がわかりますね。

BINARY=8aaaf200-2450-11e4-abe2-0002a5d5c51b
-include $(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk

sub.mk はLinuxのMakefileの書き方に近い書き方で、コンパイルすべきソースコードやインクルードパスを明示する必要があります。 TA-devkitがsub.mkに従ってビルドに必要な情報を変数に集めていくようですね。

hello_worldの例

global-incdirs-y += include
srcs-y += hello_world_ta.c

メインとなるコードにはTAに要求される以下の5つのエントリポイントを用意する必要があります。 hello_world_ta.cにも実装されていますね。

  • TA_CreateEntryPoint
  • TA_DestroyEntryPoint
  • TA_OpenSessionEntryPoint
  • TA_CloseSessionEntryPoint
  • TA_InvokeCommandEntryPoint

これらエントリポイントに加えて、TA側で利用できるTEE API群が TEE Internal Core API Specification にて定義されています。 TAを実装する方は参考にしていただければと思います。

Global Platformから出ているTEE Internal Core API Specificationはversion 1.2.1が最新ですが、OP-TEEはTEE Internal Core API v1.1.xを実装しているようです

TA側の処理を改造してみる

せっかくなのでTA側の処理も少し変更してみましょう。 本当はセキュリティに関わる処理をしたかったのですが、 あまり簡単にできる事が思いつかないのでとりあえずフィボナッチ数の計算でもやらせてみます。

optee-examples/hello_world/ta/hello_world_ta.cに以下の関数を追加してみます。

static TEE_Result fibonacci(uint32_t param_types,
       TEE_Param params[4])
{
    uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INOUT,
                                               TEE_PARAM_TYPE_NONE,
                                               TEE_PARAM_TYPE_NONE,
                                               TEE_PARAM_TYPE_NONE);
    DMSG("has been called");
    if (param_types != exp_param_types)
        return TEE_ERROR_BAD_PARAMETERS;

    IMSG("Got value: %u from NW", params[0].value.a);
    int n = params[0].value.a;
    int f[2] = {0, 1};
    if (n <= 1) {
       params[0].value.a = f[n];
    } else {
        for (int i = 2; i <= n; i++) {
            params[0].value.a = f[0] + f[1];
            f[0] = f[1];
            f[1] = params[0].value.a;
        }
    }
    IMSG("Increase value to: %u", params[0].value.a);
    return TEE_SUCCESS;
}

つづいて、同ファイルのTA_InvokeCommandEntryPoint関数にTA_HELLO_WORLD_CMD_INC_VALUEが渡ったときの呼出先を、 inc_value関数からfibonacci関数に差し替えてみます。

 */
TEE_Result TA_InvokeCommandEntryPoint(void __maybe_unused *sess_ctx,
                        uint32_t cmd_id,
                        uint32_t param_types, TEE_Param params[4])
{
        (void)&sess_ctx; /* Unused parameter */

        switch (cmd_id) {
        case TA_HELLO_WORLD_CMD_INC_VALUE:
            return fibonacci(param_types, params); // modified: inc_value -> fibonacci 
        case TA_HELLO_WORLD_CMD_DEC_VALUE:
            return dec_value(param_types, params);
        default:
            return TEE_ERROR_BAD_PARAMETERS;
        }
}

ここまで編集したら optee-qemu/build に戻り、 さきほどと同じようにもう一度make runしビルドを走らせます。 qemuが起動したらcを打ち込み、Normal WorldのターミナルでLinuxが起動したら改造したアプリを走らせてみましょう。

# optee_example_hello_world 
Invoking TA to increment 42
TA incremented value to 267914296

確かにフィボナッチ数が計算されました。 表示されるメッセージを何も変更してないのでインクリメントされすぎな感じですが。

Normal WorldからSecure Worldのメモリの中身を覗いてみる

fibonacci関数が実行されるのはSecure Worldであり、このメモリ空間はNormal Worldからは物理的に隔離されている設定です(今回はQEMU上で実行しているのであくまでもエミュレーションですが)。 さきほど作成したfibonacci関数には負の値を渡すとと配列を超えた範囲にアクセスできるという問題があります。 ここに注目して、Normal Worldからの負の値の入力を渡すと何が返ってくるのか確かめてみたいと思います。

optee-examples/hello_world/host/main.cのTEEC_InvokeCommand渡すパラメータを負の値に変更してみます。

        /*
         * Prepare the argument. Pass a value in the first parameter,
         * the remaining three parameters are unused.
         */
        op.paramTypes = TEEC_PARAM_TYPES(TEEC_VALUE_INOUT, TEEC_NONE,
                                         TEEC_NONE, TEEC_NONE);
        op.params[0].value.a = -1; // modified: 42 -> -1

        /*
         * TA_HELLO_WORLD_CMD_INC_VALUE is the actual function in the TA to be
         * called.
         */
        printf("Invoking TA to increment %d\n", op.params[0].value.a);
        res = TEEC_InvokeCommand(&sess, TA_HELLO_WORLD_CMD_INC_VALUE, &op,
                                 &err_origin);

再ビルドして、実行してみると、次のような結果になりました。

# optee_example_hello_world 
Invoking TA to increment -1
TA incremented value to 1360504
# optee_example_hello_world 
Invoking TA to increment -1
TA incremented value to 1684088
# optee_example_hello_world 
Invoking TA to increment -1
TA incremented value to 1479288
# 

配列の範囲外にアクセスしたときに何かの特別な保護がかかるかもしれない、と少し期待しつつ試してみたのですが、 Secure Worldのスタック上の不定な値が返ってきているように見えています。 また、渡す値を変えると、Secure World内のメモリの内容を広範囲に渡って見れそうです。 こうなってしまうとせっかくの隔離環境が台無しですね。

Normal Worldでのプログラミングで気をつけるべき内容は、Secure Worldのプログラミングにおいてもやはり気をつける必要がありそうです。

終わりに

本記事ではTEE規格について簡単に紹介し、 OP-TEEを使ってNormal Worldで動くアプリケーションがSecure World上で動くTAとどのようなAPIを使ってやりとりしているのかを説明しました。 また、TAの処理を改造し、TEE上でプログラムがどのように動くのか確認しました。

組み込みシステムというと確かに小さなシステムではあるのですが、 完成したシステムを組み上げるためには特有の知識やツールが沢山必要になります。 さらにSecure World上で実行されるプログラムとなるとあまり直接触れる機会は少ないですよね。 しかし、この記事で書いたように、自分でSecure Worldのプログラムを書いて、動かす、といった環境はありますし、 これからもっとやりやすくなるように整備されていくかもしれません。

今回この記事を読んでTEEに興味を持っていただいた方には、是非、お手元のRaspberry PiなどでSecure World Programmingに挑戦していただければと思います。