Rust SGXプログラミングの基本 | Anonify解体新書1
TL;DR
セキュリティ・プライバシー保護技術Anonifyの主要技術要素について解説する連載記事(全8回)
Anonifyで使用されている技術要素を洗い出し重要な技術について簡単なサンプルプログラムを交えて解説する。
その1では「Rust SGXプログラミングの基本」と題してRust SGX SDKの開発環境の構築とRust SGX SDKを使ったECall/OCallプログラムについて解説する。
ここで使用するコードはすべて独立して動作するのでAnonify自体の知識やAnonifyの動作環境は不要
サンプルコード
Anonifyが使用している主な技術要素
TEE(Intel SGX)関連
OCall/ECall <- 今回解説するのはここ
Remote Attestation
crypto_box(NaCl)
データのシーリング
mutual-TLS
Blockchain関連
スマートコントラクト
Web3
Anonifyとは
Anonifyはプロセッサのセキュリティ機能であるTEE (Trusted Execution Environment) を活用したブロックチェーンベースのプライバシー保護技術
データの秘匿化と透明性を両立させる技術
データの秘匿化 = TEE
Intel SGXを使って実現している
状態データの透明性 = TEE$ \timesブロックチェーン
イーサリアムベースのプライベートチェーンの使用を想定している
ブロックチェーン単体で担保できてるというよりはTEEの Execution Integrity と組み合わせることで実現している
MySQLやPostgresSQLのような普通のRDB使ってもできるけどブロックチェーンを使った方が楽
cipepser.icon execution integrityはSGX由来ですね
glassonion1.icon ありがとうございます。execution integrity=データの透明性ってことですか?この辺りまだ雑理解で恐縮です。
cipepser.icon 実行するコードの透明性ですね
glassonion1.icon 状態遷移を使いたい -> BCで状態を合意する
glassonion1.icon TEEのインテグリティを使ってデータベースの状態を正しく把握する
glassonion1.icon 状態データの透明性 -> 状態が重要
Anonifyリポジトリ
Anonify論文
TEEを実現するIntel SGX
TEEとはOSから独立してクリティカルな処理を安全に実行するための技術の総称
TEE = Trusted Execution Environment
TEEは data at rest data in transit data in useのうちdata in useを保護する技術
data at rest -> 格納された状態
data in transit -> 伝送中
data in use -> ユーザーによる操作中
TEEの概要についてはこの記事がおすすめ
Enclaveと呼ばれる特別に隔離されたメモリ領域上でプログラムを動作させる
SGXプログラミングはEnclaveの外とEnclaveの中を行ったり来たりする
Enclaveの中ではシステムコールが使えない
Enclaveの外側と内側をECall/OCallというEnclave内外の境界を跨ぐ関数呼び出しを使ってやりとりする
ECall/OCallを使うにはEDLというIntel独自の定義言語を使う。EDLのコンパイルにはedger8rという専用のツールが必要
本家IntelのSGX SDK
C/C++
Intel SGXの概要についてはこの記事がおすすめ
SGXプログラミングの3大潮流
Enclave内でLibraryOSを起動してそこでプログラムを動作させる
Occlum/Grapheneなど
システムコールはEnclave内のLibraryOSが代わりに実行する
アプリケーション自体をEnclave内で実行する
SCONE/VC3など
システムコールはEnclaveの外のOSに依存する
アプリケーションからクリティカルな部分を分離して実行する
Intel SGX SDK(linux-sgx)
Rust SGX SDK
Openenclave
プログラム開発が大変なものの使えるEnclaveメモリは一番多い
AnonifyはRust SGX SDKを使っている
Rust SGX SDKに関してはこのライブラリなしではRustでSGXのプログラム書くのは無理というくらい重要
OcclumとかLibOS系を使うという手もあるけど
mesalock-linuxも地味に重要でEnclave内でいろんなライブラリを使えるようにラップしてくれている
rand-sgx
anyhow-sgx
serde-sgxなど、その他多数
SGXを使ったプログラムはなんで難易度が高いのか
Enclave内でOSのシステムコールが使えないつまりglibcが使えない
システムコールが使えない、printfすら満足にできない世界
Rustでプログラム開発する場合Enclaveの中でstdが使えない
Rust SGX SDKがstdに準ずるライブラリsgx_tstdを用意してくれている
sgx_tstdはstdを完全にカバーできているわけではない
no_stdなライブラリかmesalock-linuxのライブラリしか使えない
SGXマシンを手に入れるのが大変
高い
cipepser.icon kimさんが見つけてくださったマシン、お手頃価格だった気がします(?)
glassonion1.icon 確かに
jkcomment.icon intel nucで検索すると色々出てきますー(高いやつは高い)
glassonion1.icon 5万円以下のマシンもある!
シミュレーションモード使えばMac上で開発できるものの全て開発できるわけではない
LibOSを使えばSGXマシンなしで開発できるんだっけ...
前置きはここまで、以降はRust SGX SDKを使ったプログラミングについて解説する
Hello Worldプログラムを書くための前準備
SGXプログラミングに必要なファイル
Enclaveの外側で動作するRustプログラム
ECallを呼んだり、OCallを受ける以外は普通のRustプログラム
各種EDLファイル
コンフィグファイル
EDL定義ファイル
LDSファイル(リンカスクリプトファイル)
署名用pem
Enclaveの中で動作するRustプログラム
コンパイルされてCのオブジェクトファイルになる
前準備
Rust SGX SDKの開発環境を整える
SGXマシンを入手するのは大変なのでまずはMacで動作する環境を整える
Rust SGX SDKの開発元、百度が作成したDockerイメージを使う
Dockerfile
Rust SGX SDKのリポジトリからsgx_edlディレクトリをダウンロードする
sgx_edlディレクトリにはコンパイルに必要なECall/OCallの定義ファイルやC言語のヘッダファイルが入っている
GitHubは特定ディレクトリのダウンロードができないのでsvn exportを使ってダウンロードする
code: Dockerfile
FROM baiduxlab/sgx-rust:latest as builder
WORKDIR /root/sgx
COPY buildenv.mk /root/sgx
RUN mkdir examples
COPY examples /root/sgx/examples
RUN apt-get update && apt-get install -y subversion
Docker Composeで環境を用意する場合は以下
code: docker-compose.yaml
version: "3"
services:
sgx:
build: .
volumes: # プログラムの置き場
- ./examples:/root/sgx/examples
environment:
LD_LIBRARY_PATH: "/opt/intel/sgx-aesm-service/aesm" # aesmの場所を設定する
# AEの動作に必要、Macで開発するときはコメントアウトする
# - "/dev/sgx/enclave"
stdin_open: true
tty: true
# aesm_serviceを起動する & bashで待ち受けする
command: bash -c "/opt/intel/sgx-aesm-service/aesm/aesm_service && /bin/bash"
aesmとは
Architectural Enclave Services Managerの略
AESMを起動することでArchitectural Enclaves(AE)各種が動作する
Architectural Enclavesについては以下で詳しく説明する
SGXマシン上でプログラムを実行するときは必ず起動すること
Macで開発する場合は起動しなくてもOK
以下のコマンドでコンテナに入ってプログラムのコンパイルと実行をする
code: bash
$ docker compose up -d
$ docker compose exec sgx bash
root@4bdb41328613:~#
OCall/Ecallを使ってHello Worldプログラムを書いてみる
Enclaveを使ったHello World的なプログラム
1. Enclaveの外からECallを使ってEnclaveの中に文字列を渡す
2. Enclaveの中に渡って来た文字列を出力する
3. Enclaveの中からOCallを使ってEnclaveの外に文字列を渡す
4. Enclaveの外で渡って来た文字列を出力する
今回紹介するサンプルコードの完全版はこちら
サンプルプログラムの構成
code: greeter
sgx-sdk/
├ Dockerfile
├ buildenv.mk # Rust SGX SDKからコピーしたファイル
├ docker-compose.yaml
├ sgx_edl/
├ ├ common/ # Rust SGX SDKからコピーしたディレクトリ
├ └ edl/ # Rust SGX SDKからコピーディレクトリ
└ example/
└ greeter/ サンプルプログラムのディレクトリ
├ Makefile
├ app/
│ ├ Cargo.toml
│ ├ build.rs
│ └ src/
│ └ main.rs
├ enclave/
│ ├ Cargo.toml
│ ├ Enclave.config.xml
│ ├ Enclave.edl # EDL定義ファイル
│ ├ Enclave.lds
│ ├ Enclave_private.pem
│ └ src/
│ └ lib.rs
└ lib/ # 空でOKコンパイル後のファイルが入る
Enclave.config.xml/Enclave.lds/Enclave_private.pemは本家Rust SGX SDKのサンプルをコピーしたもの
Enclave_private.pemはサンプルのために用意したもので本来は公開してはいけないファイル
Makefileは本家Rust SGX SDKのサンプルを元にいらない部分を削った
Rustのプログラムのコンパイルに加えedger8rでEDLファイルのコンパイルなんかもしている
かなり複雑なので細部まで理解する必要はない
プログラムの関係
App、Enclaveの外側のプログラム
OCallの実装はApp側に存在する
Enclave、Enclaveの中のプログラム
ECallの実装はEnclave側に存在する
AppとEnclaveは一つのプロセスで実行される
ECallとOCallプロセス間通信ぽいけど実は同じプロセスで動いてる
プログラムの関係(図はglassonion1.iconが描いたオリジナル)
https://gyazo.com/349a6a8f77fb372baf8ac8ca965b5c63
cipepser.icon 図は出典を書きましょうかね
glassonion1.icon私がPlantUMLで描いたオリジナルの図です
cipepser.icon すごい!
プログラムの流れ
https://gyazo.com/a4b7a8e6c3b0bfe30095850518074b32
SgXEnclaveのインスタンスが生きている間はEnclaveにアクセスできるイメージ
Enclaveの外側のプログラム
code: main.rs
use sgx_types::*;
use sgx_urts::SgxEnclave;
use std::slice;
static ENCLAVE_FILE: &'static str = "enclave.signed.so";
// OCall関数の実装
pub extern "C" fn ocall_pong(message: *const u8, len: usize) {
let str_slice = unsafe { slice::from_raw_parts(message, len) };
println!("{}", String::from_utf8(str_slice.to_vec()).unwrap());
}
// ECall関数の定義
extern "C" {
fn ecall_ping(
eid: sgx_enclave_id_t,
retval: *mut sgx_status_t,
message: *const u8,
len: usize,
) -> sgx_status_t;
}
// Enclaveの初期化処理
fn init_enclave() -> SgxResult<SgxEnclave> {
let mut launch_token: sgx_launch_token_t = 0; 1024; let mut launch_token_updated: i32 = 0;
// デバッグフラグ
let debug = 1;
let mut misc_attr = sgx_misc_attribute_t {
secs_attr: sgx_attributes_t { flags: 0, xfrm: 0 },
misc_select: 0,
};
// Enclaveの生成
SgxEnclave::create(
ENCLAVE_FILE,
debug,
&mut launch_token,
&mut launch_token_updated,
&mut misc_attr,
)
}
// プログラムのエントリーポイント
fn main() {
// Enclaveの初期化
let enclave = match init_enclave() {
Ok(r) => r,
Err(x) => {
println!("- Init Enclave Failed {}!", x.as_str()); return;
}
};
let mut retval = sgx_status_t::SGX_SUCCESS;
let msg = String::from("ping");
// ECall関数の呼び出し
let result = unsafe {
ecall_ping(
enclave.geteid(),
&mut retval,
msg.as_ptr() as *const u8,
msg.len(),
)
};
// ECallの呼び出しが成功したかどうか
if result != sgx_status_t::SGX_SUCCESS {
println!("- ECALL Enclave Failed {}!", result.as_str()); return;
}
// ECall関数の戻り値の検証
if retval != sgx_status_t::SGX_SUCCESS {
println!("- ECALL Enclave Failed {}!", retval.as_str()); return;
}
println!("+ greeter success..."); // Enclaveの破棄
enclave.destroy();
}
Enclaveの外側のプログラムmain.rsの流れ
Enclaveの初期化処理
ここは毎回
ECall関数の呼び出し
main.rsにあるECall関数の定義とEDLの定義は引数が微妙に違う
引数のeidとretvalは固定の引数、この2つの引数の後に本来(EDL)の引数を定義する
文字列をポインタに変換して渡す、ポインタのサイズを渡す
ECall関数の戻り値の検証
EDL関数の結果はretvalにセットされるので注意
ECall呼び出しの戻り値はECall関数の戻り値ではない
ややこしい
ここを理解してないとECallのプログラムがエラーになっていても気づけない
ハマりポイント
cipepser.icon Cだとよくあるパターンですが、RustとかGoやってると馴染みないですよね。。。
glassonion1.icon 戻り値がなければ理解できるのですが。。。ちなみにRust SGX SDKのサンプルコードはエラーチェックがバグってます(retvalのチェックをしていない)。
Enclaveの破棄
EDLの定義
code: Enclave.edl
enclave
{
/*Rust SGX SDKから提供されるEDLのインポート*/
from "sgx_tstd.edl" import *;
from "sgx_stdio.edl" import *;
from "sgx_backtrace.edl" import *;
from "sgx_tstdc.edl" import *;
trusted
{
/*ECall関数の定義*/
public sgx_status_t ecall_ping(in, size=len const uint8_t* message, size_t len); };
untrusted
{
/*OCall関数の定義*/
void ocall_pong(in, size=len const uint8_t* message, size_t len); };
};
trusted以下にECall関数の定義をuntrusted以下にOCall関数の定義を書く
ECall/OCallともにデータをポインタで渡すときはin/outの指定と渡すデータのサイズを指定する
in 参照渡しのイメージ
out データをEnclave内で生成してEnclaveの外に渡す
Enclaveの中のプログラム
code: lib.rs
extern crate sgx_tstd;
use sgx_tstd::{slice, string::String};
use sgx_types::sgx_status_t;
extern "C" {
// OCALLS
pub fn ocall_pong(message: *const u8, len: usize);
}
pub extern "C" fn ecall_ping(message: *const u8, len: usize) -> sgx_status_t {
let str_slice = unsafe { slice::from_raw_parts(message, len) };
let message = String::from_utf8(str_slice.to_vec()).unwrap();
println!("{}", message);
let msg = String::from(message + " pong");
unsafe {
ocall_pong(msg.as_ptr() as *const u8, msg.len());
}
sgx_status_t::SGX_SUCCESS
}
Enclaveの中のプログラムlib.rsの流れ
引数に渡ってきたポインタ変数をスライスに変換する
変換したスライスを文字列に変換する
文字列を出力する
文字列変数を宣言し生成する
ECall関数の呼び出し
補足
extern crate sgx_tstd はRust SGX SDKが提供するRustのstdに相当するもの
println! などマクロを使う場合は #[macro_use] の宣言が必要
Enclaveの外から渡ってきた message を slice::from_raw_parts を使ってスライスに変換する
変換したスライスを String::from_utf8 を使って文字列に変換する
ECall/OCallの引数についてプリミティブ型を除き基本バイナリしか渡せないのが辛い
ちなみにC++でプログラムする場合はchar*を渡せるっぽい
コンパイルとプログラムの実行
コマンド
code: bash
$ docker compose up -d
$ docker compose exec sgx bash
root@4bdb41328613:~# cd sgx/example/greeter
root@a712353de5b6:~/sgx/examples/greeter# make
... 省略 ...
root@a712353de5b6:~/sgx/examples/greeter# cd bin
root@a712353de5b6:~/sgx/examples/greeter/bin# ./greeter
コンパイルの流れ
1. edger8rを使ってEDLからCのプログラムを生成する
enclaveディレクトリにEnclave_t.hとEnclave_t.cが生成される
appディレクトリにEnclave_u.hとEnclave_u.cが生成される
2. appディレクトリ以下のEnclave_u.cをコンパイルする
Enclave_u.oが生成される
3. Enclave_u.oファイルから静的ライブラリファイルを生成する
libディレクトリ以下にlibEnclave_u.aが生成される
4. app/srcをcargo buildする
libEnclave_u.aをリンクに指定する
binディレクトリ以下に実行ファイルgreeter(拡張子なし)が生成される
5. enclaveディレクトリのEnclave_t.cをコンパイルする
enclaveディレクトリにEnclave_t.oが生成される
6. enclave/srcをcargo buildする
libディレクトリ以下にlibenclave.aが生成される
7. enclave/Enclave_t.oとlib/libenclave.aをリンクする
enclave/enclave.soが生成される
8. sgx_signコマンドを使ってenclave.soを署名する
binディレクトリにenclave.signed.soが生成される
コンパイルの結果bin以下に greeter と enclave.signed.so が生成される
greeterが実行ファイル、実行時にenclave.signed.soを参照する
greeterがEnclaveの外側で動作するプログラム、enclave.signed.soがEnclaveの中で動作するプログラム
enclave.signed.soはIntelの署名済みファイル
まとめ
データの秘匿化
TEE
状態データの透明性
TEE$ \timesブロックチェーン
TEEの実行環境としてIntel SGXが存在する
SGXのプログラムをRustで書いてみる
Rust SGX SDKを使ってみる
コンパイルするまでの前準備が大変だが、一度つくってしまえばなんとかなる
ECall/OCallはSGXプログラミングの基本なんで仕組みと実装方法を理解するのが重要
SGXプログラミングは日本語の情報が少なく開発環境を構築してコンパイルを通すまでが大変でした。SGXのドキュメントを読んで理解するよりは実際に開発環境を構築してプログラムを書いた方が圧倒的に理解しやすかったです。SGXのプログラミングというと専用ハードが必要なイメージがありますが、シミュレーションモードをうまく使えばMacだけで十分開発ができます。この記事を読んで興味を持たれた方がいればぜひSGXプログラミングにチャレンジしてみてください。(文責・藤田) Anonify解体新書 | 連載一覧(全8回)