こんにちは!LayerX LabsでAnonifyを開発している恩田(さいぺ)です。
Anonifyについて

Anonifyは「秘匿性」と「透明性」という相反する2つの性質を両立する秘匿化モジュールです。社内ではこの2つの性質を「Confidentiality」と「Execution Integrity」として整理しています。
要素技術としてはTrusted Execution Environment(TEE)を使っており、AnonifyのバックエンドはIntel SGX®︎を採用しています。
Intel SGXがもたらすセキュリティ・プロパティの観点では、ハードウェアレベルでの分離およびメモリ暗号化で「Confidentiality」を、Remote Attestationで「Execution Integrity」を実現します。
SGX環境で開発する難しさ
SGX内で実行されるプログラムはユーザモードでしか動作が許されていません。カーネルモードの権限を持ち合わせていないため、システムコールなどの特権命令を発行することができません。当然、libcも使えず、スレッド、ファイルI/O、ネットワークI/Oを行う際にも大きな制約が課された状態で開発することになります。Anonifyの開発にはRustを採用していますが、標準ライブラリであるstdを使うことができません。
このような制約のため、Anonifyの開発はno_std環境での開発となります。組み込みの開発と似ていますね。幸い、
SDK for Intel® Software Guard Extensions をRust向けにwrapしたRust SGX SDK が存在し、stdに相当するsgx_tstd があるのでこちらを利用して開発を進めています。より詳細を知りたい方はRustによるIntel SGXプログラミングとSDKの内部実装 - Qiitaを参照してください。
sgx_tstdを用いることで、RustによるSGXプログラミングが可能になるわけですが、今度は別の問題が発生します。Anonifyで扱うコードはすべてがSGX内で実行されるわけではないことです。
例えば、Anonifyに対してリクエストを送る場合、クライアントサイドで認証付き公開鍵暗号 でリクエストを暗号化します。暗号化に用いた公開鍵に対応する秘密鍵はSGX内部で生成されたもので、SGXの外に出ることは一切ありません。これによってConfidentialityが達成されるわけですが、暗号化はSGXの外部(クライアントサイドなのでstd環境)で行い、復号はSGXの内部(sgx_tstd環境)で行うという状況が発生します。
Anonifyでは認証付き公開鍵暗号をSodiumで実装している1ため、frame-sodiumというcrateが存在します。frame-sodium crateは、SGXの内部・外部のどちらでも利用したいのですが、カジュアルに実装すると以下のようなduplicate lang itemのエラーに遭遇することになります。
error: duplicate lang item in crate `std` (which `hoge` depends on): `f32_runtime`. | = note: the lang item is first defined in crate `sgx_tstd` (which `frame_sodium` depends on) = note: first definition in `sgx_tstd` loaded from /root/anonify/target/debug/deps/libsgx_tstd-cded739c2546dafd.rmeta = note: second definition in `std` loaded from /root/.rustup/toolchains/nightly-2020-10-25-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-3010daceac92f8fa.rlib
f32_runtimeのようなprimitiveなitemがstdとsgx_tstdの両方で定義されるため、コンパイルエラーとなってしまっています。
featureによる解決
Anonifyではこの課題を解決するためにfeaturesを活用しています。featuresはCargoが提供する条件付きコンパイルと依存選択のメカニズムで、Cargo.tomlのfeaturesセクションにfeatureを定義することができます。
AnonifyではSGX内部・外部どちらでも利用するcrateに対して、以下のようなfeaturesを設定しています。stdがSGX外で利用する場合、sgxがSGX内で利用する場合に有効化するfeatureです。
[features] default = ["std"] std = [] sgx = [ "sgx_tstd" ]
lib.rsで、以下のようにfeatureに応じて、stdを利用するのかsgx_tstdを利用するのか切り替えています。
#[cfg(feature = "std")] use std as localstd; #[cfg(feature = "sgx")] #[macro_use] extern crate sgx_tstd as localstd;
このようにlib.rsで定義しておくことで、実装時に都度切り替える手間を削減しています。例えば、Vecを利用したい場合は以下のようにstd、sgx_tstdをまとめて扱えます。
use crate::localstd::vec::Vec;
featureに応じてcrateを利用する
上述のframe-sodiumcrateは、defaultをstdとしているため、SGX外で利用する場合は以下のようにdependenciesを設定します。
[dependencies]
frame-sodium = { path = "../../../frame/sodium" }
逆に、SGX内で利用する場合は、defaultのstdを無効化し、sgxを選択します。
[dependencies]
frame-sodium = { path = "../../../frame/sodium", default-features = false, features = ["sgx"] }
Cargo.tomlでどちらのfeatureを使うか選択するだけで、実際のコードでは特に意識することなく、frame-sodiumを利用することができます。
featuresの実装
利用する側だけでなく、使われる立場であるframe-sodium側の実装を簡単に紹介します。端的には、featureごとにcfgアトリビュートを付与します。例えば、#[cfg(feature = "sgx")を付与したitemはsgx featureが有効な場合にのみコンパイルされます。
この itemに対して有効 というのは非常に強力で、関数の引数や構造体のメンバ変数を特定のfeatureでのみ有効にするといったことも可能です。
cfgアトリビュートが登場する例としては、テスト関数に対して付与することも多いと思います。Rustに触れたことがあれば、テスト関数に対して#[cfg(test)]を付与するコードを目にしたことがある方も多いのではないでしょうか。
crate作成者が開発中にアトリビュートを設定する場合は、特定のfeatureでのみ使う関数に付与することが多いと思います。Anonifyのframe-sodium crateにおいては、復号はSGX内でのみ行えることを保証したいため、decrypt関数に対して以下のようなアトリビュートを付与しています。
#[cfg(any(all(feature = "std", test), feature = "sgx"))] pub fn decrypt(&self, my_priv_key: &SodiumPrivateKey) -> Result<Vec<u8>> { let cbox = CryptoBox::new(&self.ephemeral_public_key.0, &my_priv_key.0); let plaintext = cbox .decrypt(&self.nonce.0, &self.ciphertext[..]) .map_err(|e| anyhow!("Failed to decrypt SodiumCiphertext: {:?}", e))?; Ok(plaintext) }
最後に
その他にもfeaturesを利用し、暗号方式をcrate利用側が選択できるようにもしています。
Rustに不慣れだと少し難しいという感じる部分もあるかと思いますが、stdとsgxで2回同じ実装をすることなく抽象化でき、それがコンパイラレベルで柔軟にできる点は非常に魅力的です。
最後に、LayerX開発チームではエンジニアブログ同様、4月からPodcastをはじめました。CTO松本がホストになり社内ゲストとトークしています。お時間が有りましたらぜひお聴きいただけると嬉しいです!
-
今後はjsなどからも暗号できるよう機能を拡充していき、より広範囲のクライアントに対応する予定です。現状、CIなどではクライアントサイドもRust実装の暗号化を行っています。↩