这一篇主要讨论如何把 Rust crate 移植到 Rust SGX 环境中。我们已经把一些相当复杂的库移植到了 Rust SGX 环境中,包括 rustls/webpki/ring 这个完整的 TLS stack。对比之下 Intel 移植的 SgxSSL 就简直是个“残疾”。这里就体现出了 Rust 相对于 C/C++ 的巨大优势:可移植性超强。
不过在讨论移植之前,先看一个极端例子:
if f.attr != 0600 then
print '打死我也不说'
else
print '我的密码是deadbeaf'
这例子以文件属性作为输入,判断其是否满足特定条件。一旦满足则吐出一个秘密。这个例子可能显得有些夸张——谁会用文件属性做输出密码的条件嘛!相比之下,下面这个例子就显得比较现实一些。
if envattr["GC_FACTOR"] != empty then
gc_factor = envattr["GC_FACTOR"]
这个逻辑看上去就合理多了是不是。通过读取环境变量的值来设定内存回收的参数,并且事实上 rpython 就是这么做的。
但是这样做不适用于 SGX enclave,因为无论是文件属性,还是环境变量,都不在 SGX 的保护范围之内。虽然 Scone 提供了一个所谓的 CAS 机制,试图让 enclave 跑在可信的环境变量设置之下,但是这依旧破坏了 SGX 的信任模型:只信任 CPU 和 Intel。攻击者完全可以通过打破 untrusted 部分的逻辑来污染文件属性、环境变量等并不被 SGX 保护的值,来干扰 SGX enclave 的执行逻辑。传统程序里广泛存在这样的逻辑,因此无论如何设计兼容层,使传统程序直接跑在 SGX enclave 的努力都是徒劳,因为他们依赖于不可信的输入。
这就又回到“可信”的讨论上来。事实上“可信”的概念并没有个特别清晰的定义。翻到了一篇文献里搜集了一些对于“可信”的定义(见表1)。其中 ISO/IEC15408 的定义比较有趣:“一个可信的组件、操作或者过程的行为在任意操作条件下是行为可预测的,并能很好地抵抗应用程序软件、病毒以及一定的物理干扰造成的破坏”。这个“可预测”就显得意味深长了。现代操作系统中,不可预测的东西实在太多。而 SGX 的设计理念就抛弃了非常多的“不可预测”,比如根本就不支持 syscall。这导致软件的移植是一件麻烦事——要重新考虑每个输入是否是可信的,包括所有的隐式输入(例如上述文件属性、环境变量等)。并且直接导致了基于“重打包”的 Graphene, Scone 并不能完全保护 SGX 程序,只是“看上去很美”。
那如何构造一个可信的 SGX enclave 呢?答案当然是使用我们的 rust-sgx-sdk 啦!我们提供了一个定制版的 sgx_tstd
用于取代传统的 std
,把不可信输入(例如fs
)都移到了 sgx_tstd::untrusted
空间下。如果把一个直接使用不可信输入的库移植进来,那么必然会在编译时报错:找不到符号。并且默认不打开 net
time
等 feature。如果要使用这些不可信输入,那么建议开发人员仔细思考并使用 untrusted
下的功能,并重新设计这部分逻辑。
关于随机数,我们现在提供了 sgx_rand
来满足部分需求。许多项目依赖的 rand
我还没来得及移植。
简单的来说,如果用 cargo 编译,那么就需要手工 clone 代码然后将 crate 处理为 Rust-SGX 适用的形式。如果用 xargo 编译,那么在 crate 没有使用不可信输入(没有使用 std::{fs,path,env,net,time}
等)的情况下),直接在 Cargo.toml
里引用然后 XARGO_SGX=1 make
就可以了。
具体来说,如果是 xargo 编译 (在 Rust SGX 项目的例子代码中是 XARGO_SGX=1 make
),则完全不用理会所需要的 crate 是不是支持 no_std
,因为 xargo 时已经用 sgx_tstd
彻底换掉了 std
,并且是一个 std
环境。而 cargo 编译时我们需要将整个 enclave 限定为 #![no_std]
然后通过一个相对比较丑陋的 extern crate sgx_tstd as std
来重新导入 std
。这里就涉及到了一个 no_std
和 std
的隐藏差别: std::prelude
。在 std
环境下,除了默认 extern crate std
之外,编译器还自动加了一行 use std::prelude::v1::*;
在每个 .rs
的开头。所以,在 Rust SGX 环境下,是要手工补上这一行的。具体怎么操作,参见后面的实例。
作为最简单的“移植”(其实也不算移植了),利用第三方库支持 no_std
的性质,可以在 Rust SGX + cargo 环境下直接引用 crates.io 上的库。这里以我常用的 itertools 为例,其 Cargo.toml
如是说:
[features]
default = ["use_std"]
use_std = []
默认是开启了 use_std
这个 feature。于是如果我们要支持 cargo 编译 enclave,就需要关掉这个默认打开的 feature,于是在 Cargo.toml
里这么引用:
[dependencies]
itertools = { version = "0.7.8" , default-features = false, features = []}
然后正常的 extern crate itertools
就可以了。
用一个递归来展示这个移植过程
def 移植(self):
if self支持 no_std then
不用修改,直接在依赖处配置好 no_std 的 features
return
# 移植依赖项 (忽略dev-dependencies)
for each dep of self.dependencies
移植 dep
# 移植自身
(1) wget 库代码 && tar xzf
(2) 编辑 Cargo.toml 修改每个依赖项为移植后的依赖项
(3) 编辑 src/lib.rs 添加特定header(见后文)
(4) 编辑每个源文件 添加 use std::prelude::v1::*;
(5) 仔细review每个使用 fs/path/net/time/env 等不可信输入的地方,修正那里的逻辑
(6) 检查每个 platform dependent 的 feature,将其固定为只适用于 linux-x86_64 的逻辑(因为 linux-SGX 就只有这个环境)
(7) 测试 `cargo build` 是否通过
return
(1) 以 http 为例。首先看到他的 crate name 是 http
,目前最新的版本是 0.1.8。那么获取其源代码的命令是:
wget https://crates.io/api/v1/crates/http/0.1.8/download -O http-0.1.8.crate
这是个 tgz 文件,可以直接用 tar -xzf
解压。
(2) 进入源代码目录后,首先用 cargo tree
(cargo install cargo-tree
安装)来看其依赖:
http v0.1.8 (file:///tmp/http-0.1.8)
[dependencies]
├── bytes v0.4.9
│ [dependencies]
│ ├── byteorder v1.2.3
│ └── iovec v0.1.2
│ [dependencies]
│ └── libc v0.2.42
├── fnv v1.0.6
└── itoa v0.4.2
[dev-dependencies]
...
于是需要先处理 iovec v0.1.2
。注意这里他依赖了 libc
。在 Rust SGX 环境下是不能使用libc
的,因为没有(手动狗头)。我们把libc
里兼容 SGX 的部分提炼出来,放入了 sgx_trts::libc
。如果这个满足不了需要的话,那么就没有办法了!
直接下载 iovec v0.1.2
的代码,分析其如何依赖于libc
:
~/iovec-0.1.2 $ grep -R libc .
./Cargo.toml:libc = "0.2"
./src/sys/unix.rs:use libc;
./src/sys/unix.rs: unsafe fn iovec(&self) -> libc::iovec {
./src/sys/unix.rs: mem::transmute(libc::iovec {
./src/sys/unix.rs: mem::transmute(libc::iovec {
./src/unix.rs:use libc;
./src/unix.rs:/// Convert a slice of `IoVec` refs to a slice of `libc::iovec`.
./src/unix.rs:pub fn as_os_slice<'a>(iov: &'a [&IoVec]) -> &'a [libc::iovec] {
./src/unix.rs:/// Convert a mutable slice of `IoVec` refs to a mutable slice of `libc::iovec`.
./src/unix.rs:pub fn as_os_slice_mut<'a>(iov: &'a mut [&mut IoVec]) -> &'a mut [libc::iovec] {
./src/lib.rs:extern crate libc;
看上去iovec
只依赖于libc::iovec
这个结构体的定义!那就好说了,sgx_trts::libc::iovec
这个我们是有的。于是可以如下修改Cargo.toml
:
[dependencies]
sgx_trts = "=1.0.1"
sgx_tstd = "=1.0.1"
这里去掉了关于 unix``windows
的feature。并且在整个项目里都要删除所有windows
下的部分,无条件保留unix
下的部分,并删除这两个feature。
(3) 在lib.rs
里做如下修改:
#![no_std]
extern crate sgx_tstd as std;
extern crate sgx_trts;
mod sys;
use std::{ops, mem};
pub mod unix;
(4) 在src/unix.rs
里做如下修改:
use std::prelude::v1::*;
use IoVec;
use sgx_trts::libc;
use std::mem;
把src/sys/mod.rs
改成:
mod unix;
pub use self::unix::{
IoVec,
MAX_LENGTH,
};
这里去掉了所有的unix
和不必要的代码。
把src/sys/unix.rs
改成:
use std::prelude::v1::*;
use sgx_trts::libc;
use std::{mem, slice, usize};
于是看起来就都处理干净了,这时候试试编译:
~/iovec-0.1.2 $ cargo b
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading sgx_tstd v1.0.1
Downloading sgx_unwind v0.0.2
Downloading sgx_tprotected_fs v1.0.1
Downloading sgx_alloc v1.0.1
Compiling cfg-if v0.1.4
Compiling libc v0.2.42
Compiling sgx_unwind v0.0.2
Compiling sgx_alloc v1.0.1
Compiling sgx_tprotected_fs v1.0.1
Compiling filetime v0.1.15
Compiling sgx_build_helper v0.1.0
Compiling sgx_tstd v1.0.1
Compiling iovec v0.1.2 (file:///tmp/iovec-0.1.2)
warning: unused import: `std::prelude::v1::*`=====================> ] 14/15: iovec
--> src/unix.rs:21:5
|
21 | use std::prelude::v1::*;
| ^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unused_imports)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 5.18s
再删掉 src/unix.rs
下的 prelude
引用,这个应该就做完了。
然后递归操作其他剩下的 crate 即可。如果想看答案的话可以参考我移植的 http, fnv, iovec, bytes。
这有一点点技巧性。首先需要明晰在 xargo 环境中是有一个所谓的 sysroot,包含了所有这个 target 下“环境自带”的 crate。在 Rust SGX 环境中的 sysroot 大概包括的 crates 可以查看这里。
以sgx_trts
为例,在cargo
环境下,需要在Cargo.toml
里显式引用sgx_trts
,但是在纯xargo
环境下则不需要。所以,同时支持 cargo + xargo
的Cargo.toml
长成这个样子:
[target.'cfg(not(target_env = "sgx"))'.dependencies]
sgx_tstd = "=1.0.1"
sgx_trts = "=1.0.1"
这里的语义不难理解。值得注意的是target_env = "sgx"
这里的值的来源是平台配置json:x86_64-unknown-linux-sgx.json
。这里声明了:"env": "sgx"
。所以在支持xargo
时还需要这个json文件的。
对于之上的(3)步,也有一些改变。不再是向上述所说直接配置为#![no_std]
,而是加入了一定的条件:
#![cfg_attr(not(target_env = "sgx"), no_std)]
#![cfg_attr(target_env = "sgx", feature(rustc_private))]
#[cfg(not(target_env = "sgx"))]
#[macro_use]
extern crate sgx_tstd as std;
extern crate sgx_trts;
这里的 rustc_private
是必须的,不然xargo
不允许从sysroot里读取sgx_trts
,而是强制用户从 crates.io 下载 sgx_trts
。
排除最后一行sgx_trts
不管,头部的5行是必须的,我把它叫做 “The Magic 5”。
对于一个具有复杂依赖关系的 Rust SGX enclave 来说,如果要用 xargo
编译,那么就一定需要为其指明 target json 和提供一个 Xargo.toml
来指导 xargo
。如果要单独测试一个 crate 是否移植成功,则可以在其代码根目录(Cargo.toml
所在的目录)下放置这两个文件,然后:
$ cargo b # 测试 cargo build
$ RUST_TARGET_PATH=$(pwd) xargo build --target x86_64-unknown-linux-sgx # 测试 xargo build
如果不指明RUST_TARGET_PATH
,则xargo
会在编译每个依赖项时去其代码根目录下寻找x86_64-unknown-linux-sgx.json
,少一个都不行。这个“默认去每个依赖项下寻找json”的行为是在xargo
的某个版本加上去的,为此我们不得不把json复制到了每个third_party的目录下……
这里以rust-crypto
为例。注意到原版rust-crypto
对于aesni
指令集的处理是根据这么写的:
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
pub mod aesni;
所有支持 SGX 的 cpu 都是有 aesni 支持的(参考 sgx-hardware),所以我们可以简单的强制开启aesni
模块。我们 Rust SGX 的编译过程中没定义target_arch
,所以就需要手工处理掉所有的 target_arch
相关判断,让这个库无条件的启用 aesni
模块。(其实加上target_arch
可能更优雅一点?)
优秀的可移植性是 Rust SGX 优于 C/C++ SGX 的最大特点。在此基础之上我们可以复用非常多的轮子来构造我们强壮的 Rust SGX enclave。我们已经移植了 rustls/webpki/ring、rust-crypto、wasmi、serde、protobuf 等相当复杂的库,提供了在 enclave 内 terminate TLS 的能力,以及执行任意 WebAssembly 程序的能力。此外,借助这个 TLS stack,在 enclave 内可以轻松构建出 https client 和 server,中间人彻底拜拜~(但是没法调试也是很蛋疼的……)