こんにちは!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-sodium
crateは、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実装の暗号化を行っています。↩