いつもお世話になっております。この記事は[Rust Advent Calendar 2019](Rust Advent Calendar 2019)の18日目の記事です。
この記事では、Rustを使ったIntel SGXプログラミングとそのRust-SGX-SDKライブラリの内部実装を紹介していきます。
Intel SGXとは
Intel SGXはセキュアハードウェアの代表例であるTEEの一種です。Intelが提供する第六世代Skylakeシリーズ以降のプロセッサに搭載しており、EPCと呼ばれる物理メモリ領域を隔離・保護しています。つまり、EPC領域へのアクセスは全てCPUがコントロールし、DMAによるアクセスもできません。(Enclaveの仮想メモリ空間のELRANGE領域がEPCへmappingされます。)また、SGXにおいてはこのメモリは暗号化されており、on-chipのキャッシュでのみ復号化され演算処理が行われます。そして、メモリ暗号化エンジン以外の多くのSGXの機能はmicrocodeで実装されています。
SGXにおける脅威モデルではOS、ハイパーバイザなどのシステムソフトウェアを一切信頼しておらず、万が一これらのシステムソフトウェアが悪意を持っていたとしても安全にアプリケーションが動作することを目指しています。
このようにTEEは安全ではないかもしれないデバイスでも安全にアプリケーションを動作させることを目指しており、信頼すべきはIntelのチップと隔離領域で動作するアプリケーションのみとしています。このような性質上、特に、IoT分野でデバイスのセキュリティ面の課題解決やBlockchain分野でのプライバシー保護への応用に注目が集まっています。
SGX Programming
SGX上での開発をしていく上では、大きく2つのアプローチが存在します。
- intel-sdk
- SDKとして提供されているツール群を活用していくアプローチ
- LibOS
- LibOSのコンテナ上で開発するアプローチ
LibOSではenclaveからのsystem callを含む命令で必要なMinimumなABIをそれぞれ独自で定義している分、標準ライブラリをそのままimportできるので既存ライブラリのコードベースをそのまま活用しやすいことにメリットがありますが、まだまだ対応している関数は少ないのでSGX SDKのEDLを利用するアプローチを紹介していきます。
また、SDK自体はC++で実装されていますが、enclave内プログラムはメモリ安全である方がベターなのでRustによりWrappingされたrust-sgx-sdkを使います。
全体像
SGXプログラミングを行う場合は大きく3つのステップに分けられます。
- EDLファイルの定義
- Enclave外コードの実装
- Enclave内コードの実装
Enclave内で扱うことのできない命令処理をEnclave外のコードで処理し、そのブリッジをEDLが行なっています。
EDLファイル
EDLはEnclaveの内側と外側を橋渡しするためのブリッジ関数のインターフェースを定義するためのファイルです。ここで定義したインターフェースがSDKのEdger8rというツールによりコード生成され、Enclave内外で呼ばれることになります。
実際のEDLは以下のように書きます。 (sgx-time.edl の例)
enclave {
include "time.h"
trusted {
/* define ECALLs here. */
};
untrusted {
int u_clock_gettime_ocall([out] int *error, int clk_id, [out] struct timespec *tp);
};
};
ここで、trustedパートとunrutedパートに分かれているのは、enclave内からenclave外へ呼び出す関数がocallとして定義し、逆にenclave外からenclave内へ呼び出す関数がecallとして定義されています。引数の[out]
で呼び出し先(Ocallの場合はHost側)のデータを参照し、[in]
で呼び出し元(Ocallの場合はEnclave側)のデータを参照します。(より詳細なEDLのSyntaxはIntel® Software Guard Extensions SDK for Linux* OSを参照ください)
Enclave内はsystem callができないno_std環境であるので、disk I/O、network I/Oなどsystem callを行いたい場合はOcallを介して呼び出す必要があります。また、ecallは保護された領域の機密性の高いデータを利用したい場合に用いられます。
それでは、SGX Programmingの概要を整理したところで具体的なイメージを掴むためにrust-sgx-sdkの標準ライブラリの中身を見ていきます。
sgx_tstd
Enclave内は上述のようにno_std環境ですが、標準ライブラリのサブセットであるsgx_tstdを以下のようにimportして使うことでEnclave内での標準ライブラリとして扱っていきます。
use sgx_tstd as std;
例えば本来であればsgx_tstd::fs::create_dir
はファイルシステムへのアクセスが必要なのでenclave内では用いることはできませんが、sgx_tstd
内部で上述のocall/ecallを用いることで開発者にとってはsystem callを含む処理も通常の標準ライブラリと同様に提供できています。
sgx_libc crateでEDLに定義されたx86_64用のocall関数を呼び出しています。例えば、sgx_tstd
で以下のようにファイル・ネットワークI/Oなどのocallを扱います。
// memory
pub fn u_malloc_ocall(result: * mut * mut c_void, error: * mut c_int, size: size_t) -> sgx_status_t;
// env
pub fn u_environ_ocall(result: * mut * const * const c_char) -> sgx_status_t;
// file
pub fn u_open_ocall(result: * mut c_int, error: * mut c_int, path: * const c_char, flags: c_int) -> sgx_status_t;
// fd
pub fn u_read_ocall(result: * mut ssize_t, error: * mut c_int, fd: c_int, buf: * mut c_void, count: size_t) -> sgx_status_t;
// time
pub fn u_clock_gettime_ocall(result: * mut c_int, errno: * mut c_int, clk_id: clockid_t, tp: * mut timespec) -> sgx_status_t;
// socket
pub fn u_socket_ocall(result: *mut c_int, errno: *mut c_int, domain: c_int, ty: c_int, protocol: c_int) -> sgx_status_t;
// async io
pub fn u_poll_ocall(result: * mut c_int, errno: * mut c_int, fds: * mut pollfd, nfds: nfds_t, timeout: c_int) -> sgx_status_t;
sgx_urts
これらのocall関数の処理が実際に実装されているのは、sgx_urts crate です。sgx_urts
はenclave外で動作するcrateなので通常のstd環境になります。上記のocallのうち、いくつかを見ていきます。
SocketのBindを行うためにocallで内部でlibc::bind
を呼び出しています。
#[no_mangle]
pub extern "C" fn u_bind_ocall(error: * mut c_int,
sockfd: c_int,
address: * const sockaddr,
addrlen: socklen_t) -> c_int {
let mut errno = 0;
let ret = unsafe { libc::bind(sockfd, address, addrlen) };
if ret < 0 {
errno = Error::last_os_error().raw_os_error().unwrap_or(0);
}
if !error.is_null() {
unsafe { *error = errno; }
}
ret
}
時刻を取得するためにocall内部でlibc::clock_gettime
を呼び出しています。
#[no_mangle]
pub extern "C" fn u_clock_gettime_ocall(error: * mut c_int, clk_id: clockid_t, tp: * mut timespec) -> c_int {
let mut errno = 0;
let ret = unsafe { libc::clock_gettime(clk_id, tp) };
if ret < 0 {
errno = Error::last_os_error().raw_os_error().unwrap_or(0);
}
if !error.is_null() {
unsafe { *error = errno; }
}
ret
}
ここまでのenclave -> ocall -> edl -> host
の流れをcreat_dir
の処理を例にとって見ていきます。
std::fs::create_dir
ファイルシステムの扱いはsystem callを必要としますが、sgx-sdk内部でOcallを呼び出すことで、アプリケーション開発者はenclave内でもディレクトリ生成を行うことが可能です。例として、このenclave内でのstd::fs::create_dir
ではどのような処理が行われているのか見ていきます。
まず、sgx_tstd
のfs moduleを見ると、内部的にsys::fs
moduleでsgx_trts::libc::ocall::mkdirが呼ばれています。
impl DirBuilder {
pub fn mkdir(&self, p: &Path) -> io::Result<()> {
let p = cstr(p)?;
cvt(unsafe { libc::mkdir(p.as_ptr(), self.mode) })?;
Ok(())
}
}
mod libc {
pub use sgx_trts::libc::*;
pub use sgx_trts::libc::ocall::{open64, fstat64, fsync, fdatasync, ftruncate64, lseek64, fchmod,
unlink, link, rename, chmod, readlink, symlink, stat64, lstat64,
fcntl_arg0, realpath, free, readdir64_r, closedir, dirfd, mkdir, rmdir, opendir, fstatat64};
}
このmkdir
の処理は、sgx_libc/src/linux/x86_64/ocall.rs で実装されており、u_mkdir_ocall
を呼び出しています。
pub unsafe fn mkdir(pathname: * const c_char, mode: mode_t) -> c_int {
let mut error: c_int = 0;
let mut result: c_int = 0;
let status = u_mkdir_ocall(&mut result as * mut c_int,
&mut error as * mut c_int,
pathname,
mode);
if status == sgx_status_t::SGX_SUCCESS {
if result == -1 {
set_errno(error);
}
} else {
set_errno(ESGX);
result = -1;
}
result
}
これはEnclave外の処理を呼び出すocall関数なので、sgx_file.edl で定義されています。
int u_mkdir_ocall([out] int *error, [in, string] const char *pathname, uint32_t mode);
そして、実際のu_mkdir_ocall
の処理はsgx_urts
crateのfile moduleで実装されています。
#[no_mangle]
pub extern "C" fn u_mkdir_ocall(error: * mut c_int,
pathname: * const c_char,
mode: mode_t) -> c_int {
let mut errno = 0;
let ret = unsafe { libc::mkdir(pathname, mode) };
if ret < 0 {
errno = Error::last_os_error().raw_os_error().unwrap_or(0);
}
if !error.is_null() {
unsafe { *error = errno; }
}
ret
}
以上は、std::fs::create_dir
の例でしたが、他の標準ライブラリの関数に対しても内部でocallの処理を行い、trsuted partsとuntrusted partsのseparationを意識しなくても開発できるようになっています。
まとめ
このようにSGX用の標準ライブラリを用いることで多くの処理が開発者自身がecall/ocallを意識せずとも用いられるようになってきています。 次回はTLS通信をSGX上でどう実装するかという具体例を紹介していきたいと思います。
皆さんもぜひ、よいSGXプログラミングライフを!