# Teaclave SGX SDK应用开发环境简介以及搭建
Intel SGX (Software Guard Extension, 软件防护扩展) 因为其较为出色的性能和安全性,是目前最为学术界和工业界关注的 TEE (Trusted Execution Environment, 可信执行环境)。Intel SGX 在内存中划分了名为 enclave(飞地)的隔离区域,用来存放敏感数据和代码。通过提供该隔离的可信执行环境,enclave 在操作系统、BIOS 和虚拟机监控器等系统软件均不可信的情况下,仍然对 enclave 内部的代码和数据提供保护,保障用户的关键数据和代码的机密性和完整性。
但如果 Intel SGX 程序仍然使用 C/C++ 这类内存不安全的语言开发的话,就会和传统软件一样面临着内存破坏漏洞的问题。对于 enclave 来说,受到的危害会更为严重,因为 enclave 中保存的多是机密数据和代码。Teaclave SGX 的主要目标就是通过使用高效的内存安全语言 —— Rust 来支持 enclave 应用程序的开发,从而在保证 Intel SGX enclave 内存安全的同时不会带来显著的性能开销。
Teaclave SGX SDK 内部结构分为三层:
- 最底层是使用 C/C++ 和汇编实现的 Intel SGX SDK。
- 中间层是 Rust 对 C/C++ 的 FFI (Foreign function Interfaces, 外部函数接口)。
- 最高层是 Teaclave SGX SDK。
Teaclave SGX SDK 应用程序开发者在进行开发时就只需要基于最上层的 Teaclave SGX SDK 来进行开发,底层的实现对于开发者来说是透明的。本文将从开发者的角度介绍基于 Teaclave SGX SDK 开发自己的应用程序的过程。
# 准备条件
- Ubuntu16.04 或者 18.04 或者 20.04 (Teaclave SGX SDK v1.1.3 中增加了对 Ubuntu 20.04 的支持)
- docker 环境
本文基于 Teaclave SGX SDK v1.1.3 提交哈希值:d107bd0718f723221750a4f2973451b386cbf9d2
# 基于 docker 配置 Teaclave SGX SDK 开发环境
首先需要用户机器 CPU 支持 Intel SGX 并且在 BIOS 上开启了 Intel SGX 支持。用户可以通过 SGX-hardware项目 或者在 Intel 官网 中搜索自己的 CPU 型号查看是否支持 Intel SGX。下图以 Intel Core i7-7700K 处理器为例,如下图所示,该机型支持 SGX。
当确定 CPU 支持 Intel SGX 之后,还需要开启 BIOS 中的 SGX 选项。CPU 上的 SGX 选项可能有 enabled
或者 software controlled
。具有 enabled
选项的主机直接在 BIOS 上选择 enabled
即可,而software controlled
表示 SGX 的开启需要由软件触发,还需通过 Intel 官方提供的 sgx-software-enable 开启。下载好 sgx-software-enable
之后,运行 Makefile
编译生成可执行代码 sgx_enable
,执行 sudo ./sgx_enable
顺利运行后重启主机,即可顺利开启 Intel SGX。
硬件条件准备完毕之后,还需要安装 Linux SGX 驱动(本实验环境的操作系统版本为 ubuntu16.04 ,安装时需要根据自己的操作系统版本号在 官网 下载对应的 Intel SGX 驱动) ,安装完毕之后需要确认 /dev/isgx
的存在。
下载 Teaclave SGX SDK 以及支持编译 SGX 设备的 docker image。
$ https://github.com/apache/incubator-teaclave-sgx-sdk
$ docker pull baiduxlab/sgx-rust
启动一个 docker,并且把 Teaclave SGX SDK 项目目录映射到 docker 中。
$ docker run -v /your/absolute/path/to/incubator-teaclave-sgx-sdk:/root/sgx -ti --device /dev/isgx baiduxlab/sgx-rust
在运行的 docker container 中启动 aesm 服务,White list update request successful for Version 语句意味着启动成功。
root@docker:/# LD_LIBRARY_PATH=/opt/intel/sgx-aesm-service/aesm/ /opt/intel/sgx-aesm-service/aesm/aesm_service &
aesm_service[17]: [ADMIN]White List update requested
aesm_service[17]: Failed to load QE3: 0x4004
aesm_service[17]: The server sock is 0x56096ab991c0
aesm_service[17]: [ADMIN]White list update request successful for Version: 103
执行 Teaclave SGX SDK 中的简单实例 helloworld ,检查是否正常运行。
root@docker:~# cd sgx/samplecode/helloworld/
root@docker:~/sgx/samplecode/helloworld# make
root@docker:~/sgx/samplecode/helloworld# cd bin/
root@docker:~/sgx/samplecode/helloworld/bin# ./app
[+] global_eid: 2
This is normal world string passed into enclave!
This is a Rust string!
[+] say_something success ...
至此,我们已经成功在自己的机器上跑起来了 Teaclave SGX SDK 的 helloworld 示例啦!
# Teaclave SGX SDK 示例 helloworld 剖析
接下来,我们通过阅读 helloworld 这个简单的例子来理解 Teaclave SGX SDK 应用程序的组织结构和运行方式。
# helloworld 目录结构
helloworld/
├── app
│ ├── app.c
│ └── app.h
├── bin
│ └── readme.txt
├── enclave
│ ├── Cargo.toml
│ ├── Enclave.config.xml
│ ├── Enclave.edl
│ ├── Enclave.lds
│ ├── Enclave_private.pem
│ ├── Makefile
│ ├── src
│ │ └── lib.rs
│ ├── x86_64-unknown-linux-sgx.json
│ └── Xargo.toml
├── lib
│ └── readme.txt
└── Makefile
helloworld 的目录结构和 Intel SGX 的 SampleEnclave 目录结构非常类似。
- app 目录中存放的是不可信部分代码,包括
main
函数以及OCALL
函数具体逻辑实现。 - enclave 目录中存放的是可信部分代码,主要是
ECALL
函数具体逻辑实现。- 不同于 SGX ,应用安全区的代码实现位于
src/lib.rs
, 该文件是整个helloworld
文件夹中唯一使用 Rust 编写的文件,程序员可以在该文件中增加需要的功能。 - 另外,enclave 文件夹下多了
Cargo.toml
,src/lib.rs
,x86_64-unknown-linux-sgx.json
,Xargo.toml
:Cargo.toml
: 项目清单文件,包括项目名称、项目版本以及依赖项等。x86_64-unknown-linux-sgx.json
和Xargo.toml
描述了用于项目交叉编译的信息。
- 不同于 SGX ,应用安全区的代码实现位于
# 重要代码文件解析
Enclave.edl
该文件规定了 Enclave 边界ECALL/OCALL
的定义。
enclave {
from "sgx_tstd.edl" import *;
from "sgx_stdio.edl" import *;
from "sgx_backtrace.edl" import *;
from "sgx_tstdc.edl" import *;
trusted {
/* define ECALLs here. */
public sgx_status_t say_something([in, size=len] const uint8_t* some_string, size_t len);
};
untrusted {
};
};
trusted {...}
中声明 ECALL
函数, untrusted {...}
中声明 OCALL
函数。本例中声明了一个 ECALL
函数 say_something
,该函数的具体实现在 src/lib.rs
中,它的参数包括 uint8_t *
类型的指针和长度参数 len
。
app/app.c
在 app/app.c
的 main
函数中有一个完整的调用 ECALL
的例子。
sgx_ret = say_something(global_eid,
&enclave_ret,
(const uint8_t *) str,
len);
这里的 say_something
似乎和 Enclave.edl
中的声明不太一样,ECALL传递参数时多了两个隐参数:enclave_eid
和 say_something
的返回值 &enclave_ret
。而 sgx_ret
表示的是 ECALL 执行是否成功,是 SGX 的返回值。
enclave/
文件夹部分
enclave/Cargo.toml
中声明了这是一个 staticlib
,表明 Enclave 在最后会被编译成一个 .a
文件,该文件会和 Intel 提供的 sgx_tstdc.a
等文件链接形成 enclave.so
,再经由 sgx_sign
工具配合 Enclave.config.xml
配置文件、Enclave_private.pem
签名私钥做签名并计算 measurement
,最后生成 enclave.signed.so
,这是 Enclave 的完全体。
enclave/src/lib.rs
pub extern "C" fn say_something(some_string: *const u8, some_len: usize) -> sgx_status_t {
let str_slice = unsafe { slice::from_raw_parts(some_string, some_len) };
let _ = io::stdout().write(str_slice);
// A sample &'static string
let rust_raw_string = "This is a ";
// An array
let word:[u8;4] = [82, 117, 115, 116];
// An vector
let word_vec:Vec<u8> = vec![32, 115, 116, 114, 105, 110, 103, 33];
// Construct a string from &'static string
let mut hello_string = String::from(rust_raw_string);
// Iterate on word array
for c in word.iter() {
hello_string.push(*c as char);
}
// Rust style convertion
hello_string += String::from_utf8(word_vec).expect("Invalid UTF-8")
.as_str();
// Ocall to normal world for output
println!("{}", &hello_string);
sgx_status_t::SGX_SUCCESS
}
该函数实现了一个简单的将 &[u8]
数组转化为字符串输出的函数,注意在函数的最后调用的 println!
函数是一个 OCALL
。 println!
的具体实现中加入了内置的 OCALL
,并定义了内置的 edl
,import到了 Enclave.edl
中。
enclave {
from "sgx_tstd.edl" import *;
from "sgx_stdio.edl" import *;
from "sgx_backtrace.edl" import *;
from "sgx_tstdc.edl" import *;
# 编译后的代码目录
经过编译之后的代码目录如下所示,这里省略了 release
文件夹下的内容。
├── app
│ ├── app.c
│ ├── app.h
│ ├── app.o #[generate]
│ ├── Enclave_u.c #[generate]
│ ├── Enclave_u.h #[generate]
│ └── Enclave_u.o #[generate]
├── bin
│ ├── app #[generate]
│ ├── enclave.signed.so #[generate]
│ └── readme.txt
├── enclave
│ ├── Cargo.lock #[generate]
│ ├── Cargo.toml
│ ├── Enclave.config.xml
│ ├── Enclave.edl
│ ├── Enclave.lds
│ ├── Enclave_private.pem
│ ├── enclave.so #[generate]
│ ├── Enclave_t.c #[generate]
│ ├── Enclave_t.h #[generate]
│ ├── Enclave_t.o #[generate]
│ ├── Makefile
│ ├── src
│ │ └── lib.rs
│ ├── target #[generate]
│ │ ├── CACHEDIR.TAG #[generate]
│ │ └── release #[generate]
│ ├── x86_64-unknown-linux-sgx.json
│ └── Xargo.toml
├── lib
│ ├── libenclave.a #[generate]
│ ├── libsgx_ustdc.a #[generate]
│ └── readme.txt
└── Makefile
helloworld 编译的基本流程类似于 Intel SGX:
edger8r
将输入的EDL
在app/
目录下生成不可信代码Enclave_u.h
和Enclave_u.c
;- 编译不可信部分生成
bin/app
; edger8r
在enclave/
目录下生成可信代码Enclave_t.h
和Enclave_t.c
;- 编译并签名生成可信动态链接库
enclave.signed.so
。
# 开发者如何开发自己的 Rust SGX Application
同样类似于开发 Intel SGX Application,用户可以通过改写 Teaclave SGX SDK 所提供的 samplecode
,在这里,我以一个简单的例子抛砖引玉。
# 添加自定义的函数
假设用户希望在 Teaclave SGX SDK 中实现一个简单的求两个数组的交集的函数,只需要直接在 src/lib.rs
中添加实现的函数。下面的示例代码 intersection
函数是希望添加的求交集函数,注意这里求到的交集结果是无重复元素的。传入的两个参数是需要求交集的 i32
向量,最后返回的是两个向量的交集。其具体的实现是通过一个额外的散列集,记录 num1
出现的元素,再对 num2
进行遍历,如果 num2
出现了散列集中的元素,则将该值 push
到交集数组中,并将散列表中的对应元素移除。当 num2
遍历完毕之后,返回交集数组。
pub fn intersection(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
use std::collections::HashSet;
let mut set: HashSet<i32> = HashSet::new();
let mut vec: Vec<i32> = Vec::new();
for i in nums1.iter() {
set.insert(*i);
}
for i in nums2.iter() {
if set.contains(i) {
vec.push(*i);
set.remove(i);
}
}
return vec;
}
考虑一个比较现实的场景,两个用户分别将自己的向量作为参数传入 enclave 中进行计算,这时候数据需要从不可信代码区域复制到可信代码区域。
首先,需要在 Enclave.edl
文件中修改 say_something
函数的定义,输入参数为两个用户的向量指针以及对应的向量大小。
public sgx_status_t say_something([in, size=len1] size_t* num1, size_t len1,
[in, size=len2] size_t* num2, size_t len2);
接着,在 app.c
文件中声明需要求交集的数组以及大小并仿照示例调用 say_something
。
size_t nums1[10] = {0,1,2,3,4,5,6,7,8,9};
size_t nums2[10] = {5,6,7,8,9,10,11,12,13,14};
size_t len1 = sizeof(nums1);
size_t len2 = sizeof(nums2);
sgx_ret = say_something(global_eid,
&enclave_ret,
nums1,
len1,
nums2,
len2);
回到 enclave/src/lib.rs
,say_something
传进来的是两个向量的起始地址以及大小。
pub extern "C" fn say_something(nums1: *mut usize, len1: usize, nums2: *mut usize, len2: usize) -> sgx_status_t
由于数据是从非安全区复制到安全区的,还需要对 intersection
函数进行部分改写。传进来的参数是数组指针,以指针地址为起始地址,根据大小参数限制迭代范围并获得一个用于循环的序号变量 i
,在 for
循环中使用 offset
偏移指针,解引用它,读出 nums1
和 nums2
的元素值。
pub fn intersection(nums1: *mut usize, len1: usize, nums2: *mut usize, len2: usize) -> Vec<usize> {
use std::collections::HashSet;
let mut set: HashSet<usize> = HashSet::new();
let mut vec: Vec<usize> = Vec::new();
for i in 0..len1/mem::size_of::<usize>() {
let mut val_nums1 = 0;
unsafe {
val_nums1 = *nums1.offset(i as isize);
}
set.insert(val_nums1);
}
for i in 0..len2/mem::size_of::<usize>() {
let mut val_nums2 = 0;
unsafe {
val_nums2 = *nums2.offset(i as isize);
}
if set.contains(&val_nums2) {
vec.push(val_nums2);
set.remove(&val_nums2);
}
}
return vec;
}
完整的 say_something
函数如下所示。
#[no_mangle]
pub extern "C" fn say_something(nums1: *mut usize, len1: usize, nums2: *mut usize, len2: usize) -> sgx_status_t {
let vec: Vec<usize> = intersection(nums1, len1, nums2, len2);
println!("intersection set is {:?}", vec);
sgx_status_t::SGX_SUCCESS
}
重新编译并运行,得到运行结果:
[+] global_eid: 2
intersection set is [5, 6, 7, 8, 9]
[+] say_something success ...
我们基于 Teaclave SGX SDK 的 helloworld 实现了自己的求交集函数。
# 调用 Teaclave SGX SDK 提供的 crate
Teaclave SGX SDK 重写了很多 SGX 的库,当我们需要用某个库时,可以先在仓库中查看是否有相应的 crate
实现以及对应的 doc。比如当我们希望生成一个随机数时,在 C++
或者 Rust
环境下,会想到使用 rand
库。自然而然地,Teaclave SGX SDK 也用 Rust 重写了 sgx_rand
库。
首先在 enclave/Cargo.toml
中的 [target.'cfg(not(target_env = "sgx"))'.dependencies]
部分添加 sgx_rand
库的地址。
[target.'cfg(not(target_env = "sgx"))'.dependencies]
sgx_rand = {git = "https://github.com/apache/teaclave-sgx-sdk.git" }
现在万事俱备,只欠调用。回到 lib.rs
文件中,链接到 sgx_rand
crate
,导入其中的所有项,声明需要使用的模块。
extern crate sgx_rand;
use sgx_rand::Rng;
use sgx_rand::os::SgxRng;
调用 gen_range
函数生成 0-10 之间的随机数。
let random = rng.gen_range(0, 10);
这样就可以在 Teaclave SGX SDK 中的 enclave 中通过调用官方 crate
随机生成一个随机数。
# 总结
本文首先介绍了 Teaclave SGX SDK 项目的基本结构,然后以 helloworld
为例子,介绍了一个简单的 Teaclave SGX SDK 的示例的组织结构和编译过程,最后,以在 helloworld
中实现 intersection
函数为例,介绍了如何基于提供的 SampleCode 进行 Teaclave SGX SDK 应用程序的开发。