Skip to content

Latest commit

 

History

History
263 lines (192 loc) · 14 KB

04.md

File metadata and controls

263 lines (192 loc) · 14 KB

移植

这一篇主要讨论如何把 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 和 Xargo

简单的来说,如果用 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_stdstd隐藏差别: std::prelude。在 std 环境下,除了默认 extern crate std 之外,编译器还自动加了一行 use std::prelude::v1::*; 在每个 .rs 的开头。所以,在 Rust SGX 环境下,是要手工补上这一行的。具体怎么操作,参见后面的实例。

直接使用支持 no_std 的 crate

作为最简单的“移植”(其实也不算移植了),利用第三方库支持 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 就可以了。

Cargo 环境下的移植

用一个递归来展示这个移植过程

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 treecargo 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

同时支持 cargo 和 xargo

这有一点点技巧性。首先需要明晰在 xargo 环境中是有一个所谓的 sysroot,包含了所有这个 target 下“环境自带”的 crate。在 Rust SGX 环境中的 sysroot 大概包括的 crates 可以查看这里

sgx_trts为例,在cargo环境下,需要在Cargo.toml里显式引用sgx_trts,但是在纯xargo环境下则不需要。所以,同时支持 cargo + xargoCargo.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的目录下……

特殊的feature处理

这里以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,中间人彻底拜拜~(但是没法调试也是很蛋疼的……)