使用 Teaclave TrustZone SDK 开发 TrustZone 应用

October 15, 2021 · Wenwen Ruan

欢迎 RUST OP-TEE TRUSTZONE SDK 成为 TEACLAVE 子项目 一文中已经对Teaclave TrustZone SDK 项目进行了简单的介绍。在本文中,将会介绍使用 Teaclave TrustZone SDK 开发 TrustZone 应用程序。

# Teaclave TrustZone SDK 应用开发环境搭建

# 准备条件

  • Ubuntu 系列

本文基于的 Teaclave TrustZone SDK 提交哈希值:8520a2018705edcebfb7e729bd2ced12414fc052

# 配置 Teaclave TrustZone SDK 编译环境

下载 Teaclave TrustZone SDK 项目,初始化相关的子模块并安装 Rust 工具链以及交叉编译工具 Xargo。

$ git clone https://github.com/apache/incubator-teaclave-trustzone-sdk
$ cd incubator-teaclave-trustzone-sdk
$ ./setup.sh

初始化 OP-TEE 子模块。初始化完毕之后,在 optee 根目录下需要有 build/, optee_os/optee_client 子目录。

$ git submodule update --init -- optee

在编译样例之前,需要设置环境变量。

$ source environment

默认情况下,目标平台是 aarch64,如果希望为 arm 平台编译,需要在 source environment 之前设置 ARCH 变量。

$ export ARCH=arm
$ source environment

接着,下载 ARM 工具链并编译 OP-TEE 库。

make optee

最后,编译 Teaclave TrustZone SDK 官方提供的例子。

make examples

# 在 QEMU ARMv8 上运行 Teaclave TrustZone SDK 应用程序

现在,Teaclave TrustZone SDK 官方提供的示例已经编译好了,但如果需要在 QEMU ARMv8 模拟器上运行这些示例,还需要准备一个支持 OP-TEE 的 QEMU 环境,从而在该环境上运行已经编译好的 SDK 中的示例。 首先,需要安装 QEMU 环境需要的依赖。

$ 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

也可以选择使用 Teaclave TrustZone SDK 官方提供的 docker,在 docker 中开发就无需下载上述依赖。

$ docker pull teaclave/teaclave-trustzone-sdk-build:0.2.1
# start docker
$ docker run -ti teaclave/teaclave-trustzone-sdk-build:0.2.1

下载 QEMU ARMv8 对应的 OP-TEE 的源代码。

$ mkdir -p ~/bin
$ curl https://storage.googleapis.com/git-repo-downloads/repo-1 > ~/bin/repo && chmod a+x ~/bin/repo
$ export PATH=~/bin:$PATH
$ mkdir optee-qemuv8-3.14.0 && cd optee-qemuv8-3.14.0 && \
  repo init -u https://github.com/OP-TEE/manifest.git -m qemu_v8.xml -b 3.14.0 && \
  repo sync -j4 --no-clone-bundle

编译 QEMU ARMv8 OP-TEE。

$ cd build
$ make -j2 toolchains && \
  make QEMU_VIRTFS_ENABLE=y CFG_TEE_RAM_VA_SIZE=0x00300000

在漫长的编译过程之后,还需要新建一个共享文件夹,用于和 QEMU 子系统共享示例的 host apps 和 TAs。

首先要将 path/to/example/host/target/aarch64-unknown-linux-gnu/release/examplepath/to/example/ta/target/aarch64-unknown-optee-trustzone/release/*.ta 分别复制到 incubator-teaclave-trustzone-sdk/out/hostincubator-teaclave-trustzone-sdk/out/ta/。接着还需要将 incubator-teaclave-trustzone-sdk/out/* 中的文件复制到 QEMU 共享文件夹 shared_folder/ 中。

$ mkdir shared_folder
$ (cd /project/root/dir/ && make examples-install)
$ cp -r /project/root/dir/out/* shared_folder/

如果处于一个没有 GUI 的运行环境,在启动 QEMU 之前,还需要修改 qemu_v8.mk 中的代码。以 OP-TEE QEMU 3.14.0 版本为例,注释掉 optee-qemuv8-3.14.0/build/qemu_v8.mk 中的 386-388 行。

.PHONY: run-only
run-only:
        ln -sf $(ROOT)/out-br/images/rootfs.cpio.gz $(BINARIES_PATH)/
        $(call check-terminal)
        $(call run-help)
        # $(call launch-terminal,54320,"Normal World")
        # $(call launch-terminal,54321,"Secure World")
        # $(call wait-for-ports,54320,54321)
        cd $(BINARIES_PATH) && $(QEMU_BUILD)/aarch64-softmmu/qemu-system-aarch64 \

在启动 QEMU 之前前,需要运行 nc 来监听端口 5432054321

$ nc -l 127.0.0.1 -p 54320
$ nc -l 127.0.0.1 -p 54321

进入 qemu_v8.mk 所在的目录启动 QEMU。

make run-only QEMU_VIRTFS_ENABLE=y QEMU_VIRTFS_HOST_DIR=$(pwd)/shared_folder

当 QEMU 启动之后,端口 54320 窗口中运行的是普通世界,端口 54321 窗口中运行的是安全世界。在普通世界中,根据提示输入 root 登录后,需要将共享文件夹挂载到 QEMU 子系统中,用于在 QEMU 中访问编译好的 CA/TA 可执行文件。

$ mkdir shared && mount -t 9p -o trans=virtio host shared

接着,需要将 TA 复制到 /lib/optee_armtz 目录下,提供给安全世界调用。

$ cd shared && cp ta/*.ta /lib/optee_armtz/

进入 host 文件夹中并执行 host apps。

$ cd host
$ ./hello_world
original value is 29
inc value is 129
dec value is 29
Success

至此,我们成功地在 QEMU 环境中运行了 Teaclave TrustZone SDK 的 hello_world-rs 示例。

# 配置 Teaclave TrustZone SDK 应用程序的 debug 环境

在开发应用程序的时候,难免会有 debug 的需求,在这不一部分,将会简单介绍如何在 Teaclave TrustZone SDK 中配置 debug 环境。

在编译 QEMU ARMv8 OPTEE 时需要关闭 ASLR,可以通过直接修改 OP-TEE/optee_os/mk/config.mk 文件中的 CFG_CORE_ASLRn,注意修改之后还需要重新编译 make run

# CFG_CORE_ASLR ?= y
CFG_CORE_ASLR ?= n

也可以直接在编译时添加编译信息: make run CFG_CORE_ASLR=n

由于程序是在远程系统上 (QEMU) 上被 debugged,所以在编译时还需要加上 GDBSERVER=y

在启动 gdb 之后,执行 target remote :1234 命令连接上 QEMU GDB 服务器端口。

$ ./path/to/qemu-v8-project/out-br/host/bin/aarch64-buildroot-linux-gnu-gdb
(gdb) target remote :1234
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xffffb30b00ea12b4 in ?? ()

接下来,加载 TEE 内核符号表。

(gdb) symbol-file /path/to/qemu-v8-project/optee_os/out/arm/core/tee.elf

hello_world-rs 为例,根据安全世界窗口提示,可知 hello_world-rs 的 TA text 部分的起始地址为 0x40014000。

D/LD:  ldelf:168 ELF (133af0ca-bdab-11eb-9130-43bf7873bf67) at 0x40014000

根据该地址提示,从该地址开始加载 hello_world-rs 的 ta 符号表。

(gdb) add-symbol-file /path/to/examples/hello_world-rs/ta/target/aarch64-unknown-optee-trustzone/debug/ta 0x40014000

然后,可以根据自己的需求在相应的函数或地址上打断点。

(gdb) b open_session

QEMU 执行示意图

# Teaclave TrustZone SDK 示例 hello_world-rs 剖析

# hello_world-rs 目录结构

├── Makefile
├── host
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── Makefile
│   └── src
│       └── main.rs
├── proto
│   ├── Cargo.toml
│   ├── build.rs
│   └── src
│       └── lib.rs
├── ta
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── Makefile
│   ├── Xargo.toml
│   ├── build.rs
│   ├── src
│   │   └── main.rs
│   ├── ta_aarch64.lds
│   ├── ta_arm.lds
│   └── ta_static.rs
└── uuid.txt

  • host 文件夹中存放的是普通世界的 untrusted code
    • host/src/main.rshello_world-rs 应用程序执行的入口,Cargo.toml 描述了 host 部分的依赖, Cargo.lock 中包含了依赖项的完整信息,Makefile 定义了 host 部分的编译信息。
  • ta 文件夹中存放的是安全世界中的 trusted code
    • 相比较 hostta 文件夹中多了以下文件:Xargo.toml 是 TA 的交叉编译文件 ,ta_aarch64.ldsta_arm.lds 分别定义了在 64 位架构和 32 位架构下 teaclave trustzone sdk 应用程序各部分在程序地址空间内的布局;ta_static.rs 定义了 TA 中的静态数据信息。
  • proto 文件夹中存放的是 CA (Client Application) 和 TA (Trusted Application) 共享的数据结构,并承担着解析 uuid.txt 提取 UUID 的任务。
  • uuid.txt 文件中记录的是 TA 的 UUID,是每个 TA 独一无二的身份标识。

# hello_world-rs 重要代码文件解析

  • host/src/main.rs

进入 main 函数,首先调用 Context::new 函数建立起 hello_world-rs CA 和 TA 的逻辑联系,ctx 指向类型为 Context 的变量的地址,用于 CA 和 TA 的连接和通信。

let mut ctx = Context::new()?;

调用 open_session 在 CA 和对应的 TA 中打开一个 session,并将 hello_world-rs 的 UUID 作为参数传入,用于指引 CA 连接对应 UUID 值的 TA。

let uuid = Uuid::parse_str(UUID).unwrap();
let mut session = ctx.open_session(uuid)?;

&mut session 作为参数传入 hello_world 函数中。

hello_world(&mut session)?;

进入到 hello_world 函数中,首先将要进行运算的 u32 操作数用 ParamValue 类型包装为操作数 p0,设置其值为29,类型为 ValueInout,表示同时作为输入参数和返回值。

let p0 = ParamValue::new(29, 0, ParamType::ValueInout);

operation 用于保存 CA 要传递给 TA 的参数信息,第一个参数一般保留为 0,由于这里只有一个要传递的参数 p0,其他参数都保留为 ParamNone

let mut operation = Operation::new(0, p0, ParamNone, ParamNone, ParamNone);

CA 端使用获取到的 session, command_id 和要传递的参数 operation 调用 invoke_command 执行特定的 command,该操作将会切换到安全世界。

session.invoke_command(Command::IncValue as u32, &mut operation)?;
  • ta/src/main.rs

ta/src/main.rs 中的 invoke_command 函数参数与 host 中调用的 invoke_command 略有不同,第二个参数是 Paramters 类型。当数据从 CA 传递到 TA 时,实际上执行的是按 bit 的复制操作,所以 params 中的数据就是从 operation 中传递过来的数据.

fn invoke_command(cmd_id: u32, params: &mut Parameters) -> Result<()> {

valuesparams 取出要操作的 u32 值,match 表达式根据传入的参数 cmd_id 匹配对应的操作。在下面的代码中,如果匹配到 Command::IncValue,就对 values 中的 u32 值执行 +100 的操作;如果匹配到 Command::DecValue,就执行 -100 的操作;如果匹配到其他值,就直接返回错误参数的错误类型。

fn invoke_command(cmd_id: u32, params: &mut Parameters) -> Result<()> {
    trace_println!("[+] TA invoke command");
    let mut values = unsafe { params.0.as_value().unwrap() };
    match Command::from(cmd_id) {
        Command::IncValue => {
            values.set_a(values.a() + 100);
            Ok(())
        }
        Command::DecValue => {
            values.set_a(values.a() - 100);
            Ok(())
        }
        _ => Err(Error::new(ErrorKind::BadParameters)),
    }
}
  • proto/src/lib.rs

lib.rs 中的枚举变量 Command 声明是开发者要实现的命令。

pub enum Command {
    IncValue,
    DecValue,
    Unknown,
}

# 编译之后的 hello_world-rs 代码目录

编译之后的代码目录如下所示,这里省略了 release 文件夹下的内容。

├── Makefile
├── host
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── Makefile
│   ├── src
│   │   └── main.rs
│   └── target                               #[generate] 
│       ├── aarch64-unknown-linux-gnu        #[generate] 
│       │   └── release                      #[generate]
│       └── release                          #[generate]
├── proto
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── build.rs
│   ├── src
│   │   └── lib.rs
│   └── target                               #[generate]
│       └── rls                              #[generate]
│           └── debug                        #[generate]
├── ta
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── Makefile
│   ├── Xargo.toml
│   ├── build.rs
│   ├── src
│   │   └── main.rs
│   ├── ta_aarch64.lds
│   ├── ta_arm.lds
│   ├── ta_static.rs
│   └── target                               #[generate]
│       ├── aarch64-unknown-optee-trustzone  #[generate]
│       │   └── release                      #[generate]
│       └── release                          #[generate]
└── uuid.txt

hello_world-rs 编译过程更类似于 Rust 程序编译。

  • 编译不可信部分 host 文件夹,生成 hello_world-rs 可执行文件;
  • 交叉编译可信部分 ta 文件夹,再用 UUID 和密钥进行签名,生成 UUID.ta 可执行文件。
  • 在执行时,hello_world-rsUUID.ta 验证通过后调用执行。

# 开发者如何开发自己的 Teaclave TrustZone SDK 应用程序

和前面介绍过的 使用 TEACLAVE SGX SDK 开发 SGX 应用 相似,这里也同样通过对 Teaclave TrustZone SDK 示例程序 hello_world-rs 进行改写来介绍如何构造自己的 Teaclave TrustZone SDK。

需要注意的是,Teaclave TrustZone SDK 是通过 UUID 唯一标识系统中的 TA,UUID 值不能重复,所以我们首先需要通过 ITU-T UUID generator 网站申请属于自己的唯一的 UUID,并将 uuid.rs 文件中的内容修改为新得到的 UUID 值。

1487a406-160d-4641-957e-66292f8d1309

假设开发目标是为两个 u8 数组求得交集和并集,也就是要实现交集函数 Intersection 和并集函数 Union 两个功能函数。

proto/lib.rs 进行修改,将 Command 中的成员替换为待实现的 IntersectionUnion

pub enum Command {
    Intersection,
    Union,
    Unknown,
}

impl From<u32> for Command {
    #[inline]
    fn from(value: u32) -> Command {
        match value {
            0 => Command::Intersection,
            1 => Command::Union,
            _ => Command::Unknown,
        }
    }
}

接着,进入 host/src/main.rs 中的 main 函数,添加进行数据计算的函数,将用于与 TA 通信的 session 内存地址作为参数传递到 data_compute 中。

data_compute(&mut session)?;

data_compute 中,首先声明要进行数据处理的两个 u8 数组 nums1nums2,以及用于存储数据处理结果的 resu。在示例代码 hello_world 中的变量声明使用的是 ParamValue,但这里我们需要访问数组,一段连续的内存变量而非变量。通过阅读 Teaclave TrustZone SDK client 端的 Rust 仓库 Crate optee_teec,可知 ParamTmpRef 用于定义临时内存访问。于是将这三个数组地址作为参数新建 ParamTmpRef 类型,并将 ParamTmpRef 类型变量传递到 operation 中,用于传递给 TA 交互信息。

在准备好与 TA 交互的信息后,调用 invoke_command 通知对应的 TA 执行 Command::Intersection 指定的操作。

// in host/src/main.rs
fn data_compute(session: &mut Session) -> optee_teec::Result<()> {
    let nums1:[u8; 5] = [1, 2, 3, 4, 5];
    let nums2:[u8; 5] = [4, 5, 6, 7, 8];
    let mut resu = vec![0; 10];

    let p1 = ParamTmpRef::new_input(&nums1);
    let p2 = ParamTmpRef::new_input(&nums2);
    let p3 = ParamTmpRef::new_output(&mut resu);
    let mut operation = Operation::new(0, p1, p2, p3, ParamNone);

    println!("intersection invoke");
    session.invoke_command(Command::Intersection as u32, &mut operation)?;
}

invoke_command 函数的具体实现在 ta/sec/main.rs 文件中的 invoke_command。共享的参数通过 params 从 CA 传递到 TA 中, 同样,可以根据 TA 端的 Rust 仓库 optee_utee 提供的接口函数抽丝剥茧般地提取出来 ParamMemref 类型的 nums1, nums2vec_resu

    let nums1 = unsafe { params.0.as_memref().unwrap().raw() };
    let nums2 = unsafe { params.1.as_memref().unwrap().raw() };
    let mut vec_resu = unsafe { params.2.as_memref().unwrap().raw() };

    let nums1_size = unsafe { (*nums1).size };
    let nums2_size = unsafe { (*nums2).size };

现在,进入 match 表达式中,将 Command::from 的枚举修改为 Command::IntersectionCommand::Union。要实现的函数就填充到对应的分支括号中。

  match Command::from(cmd_id) {
    Command::Intersection => {
      Ok(())
    }
    Command::Union => {
      Ok(())
    }

下面的示例代码实现的是求两个数组之间的交集元素。具体的实现是通过一个额外的散列集 set,记录 nums1 中的所有元素,然后对 nums2 中的元素进行遍历,如果 nums2 中的元素也出现在了 set 中,那么该元素为 nums1nums2 共有,是交集元素,写入结果向量 vec_resu 中,并移除掉 set 中的该元素。最后,将结果向量的 size 修改为共有的交集元素的个数。其中,要读取 nums1nums2 数组中的元素,还需要解引用 ParamMemref 类型的指针读取出指向元素值的 buffer 指针地址,再使用 offset 偏移指针从而读出 nums1nums2 的值。

      let mut set: HashSet<u8> = HashSet::new();
      let mut vec_count = 0;
      for i in 0..nums1_size {
        let mut val_nums1 = 0;
        unsafe {
          val_nums1 = *((*nums1).buffer as *mut u8).offset(i as isize);
        };
        set.insert(val_nums1);
      }

      for i in 0..nums2_size {
        let mut val_nums2 = 0;
        unsafe {
          val_nums2 = *((*nums2).buffer as *mut u8).offset(i as isize);
        };

        if set.contains(&val_nums2) {
          unsafe { *((*vec_resu).buffer as *mut u8).offset(vec_count as isize) = val_nums2; }
          vec_count += 1;
          set.remove(&val_nums2);
        }
      }
      unsafe{ (*vec_resu).size = vec_count; }

对于 Union 函数的实现,同样是利用一个额外的散列集 set,记录 nums1 中的所有元素,并直接将 nums1 中的元素写入结果向量 vec_resu 中,而后再依次读取 nums2 中的元素,如果该元素没有在 set 中出现,则写入结果向量 vec_resu 和散列集 set 中。

      let mut set: HashSet<u8> = HashSet::new();
      let mut vec_count = 0;
      for i in 0..nums1_size {
        let mut val_nums1 = 0;
        unsafe {
          val_nums1 = *((*nums1).buffer as *mut u8).offset(i as isize);
          *((*vec_resu).buffer as *mut u8).offset(vec_count as isize) = val_nums1;
        }
        vec_count += 1;
        set.insert(val_nums1);
      }

      for i in 0..nums2_size {
        let mut val_nums2 = 0;
        unsafe {
          val_nums2 = *((*nums2).buffer as *mut u8).offset(i as isize);
        };

        if !set.contains(&val_nums2) {
          unsafe { *((*vec_resu).buffer as *mut u8).offset(vec_count as isize) = val_nums2; }
          vec_count += 1;
          set.insert(val_nums2);
        }
      }
      unsafe{ (*vec_resu).size = vec_count; }

回到 host/src/main.rs,通过 updated_size 函数读取到在 ta/src/main.rs 中对 vec_resu 新设置的 size 值,也就是 nums1nums2 共有的元素的个数,最后打印出结果向量 resu 的值。

    // in data_compute function
    let updated_size = operation.parameters().2.updated_size();
    println!("Intersection resu = {:?}", &resu[..updated_size]);

这样,我们就基于 Teaclave TrustZone SDK 提供的示例代码实现了自己的求交集和并集函数。

# 总结

本文首先介绍 Teaclave TrustZone SDK 项目的环境配置过程,然后介绍了简单示例 hello_world-rs 的组织结构和编译过程 ,最后,通过修改 hello_world-rs 实现 intersectionunion 函数为例,介绍如何基于提供的 SampleCode 进行 Teaclave TrustZone SDK 应用程序的开发。

# 延伸阅读