Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

高速な暗号実装のためにしてきたこと

599 views

Published on

MANABIYA

Published in: Technology
  • Be the first to comment

高速な暗号実装のためにしてきたこと

  1. 1. 高速な暗号実装のためにしてきたこと MANABIYA 2018/3/24 光成滋生
  2. 2. • サイボウズ・ラボで暗号とセキュリティのR&D • 『クラウドを支えるこれからの暗号技術』 • 『パターン認識と機械学習の学習』 光成滋生(@herumi) 2 / 40
  3. 3. • 高機能暗号の紹介 • 自作アセンブラの紹介 • ソフトウェア開発の中で私が経験したことの紹介 • ソフトウェア開発で考えること・気を付けていること • 印象に残ってるバグ • まとめ 講演の目的 3 / 40
  4. 4. • 従来の古典的な暗号に無い機能を持つ暗号の総称 • 定義の決まった用語ではない • クラウドサービスでの利用を想定したものが多い • 秘匿検索 • キーワードを暗号化したまま検索できる • 準同型暗号 • 暗号化したまま計算できる • 属性ベース暗号 • 属性に応じた復号権限を制御できる • etc. 高機能暗号 4 / 40
  5. 5. • 普通の暗号文は復号以外の操作を何もできない • レベル2準同型暗号 • 暗号文(カプセル)を開けずに積和演算ができる • アンケートの平均、分散、クロス集計などが可能 準同型暗号 123 +×1 3 42 × 14= 5 / 40
  6. 6. デモ
  7. 7. • 理論 • ペアリングベースによる効率のよい手法の提案 • 国際学会に採択(6月頃発表予定) • 実装 • https://github.com/herumi/mcl • C++/Xbyak/LLVMによるサーバ向け実装 • https://github.com/herumi/she-wasm • JavaScript(WebAssembly)によるブラウザ向け実装 • https://herumi.github.io/she-wasm/she-demo.html (デモ) 理論と実装 7 / 40
  8. 8. • 個別のデータの中身を見ないで解析し 必要な情報のみを抽出する技術 • 産業技術総合研究所・早稲田大学 基幹理工学部 情報理工学科 • 実装エンジンに利用されている • 秘匿ゲノム検索技術 • 化合物類似構造秘匿検索 • https://www.waseda.jp/fsci/giti/assets/uploads/2017/04/01ef9 9e5e54050220ce0ff592104af39.pdf 33ページより引用 応用例1(プライバシー保護データ解析) 8 / 40
  9. 9. • Zcash(暗号通貨) • https://z.cash/ • 取り引きの追跡ができない匿名性の強い通貨 • ペアリングベースのゼロ知識証明(zkSNARKs)を用いて実現 • 入力を隠したまま正しい計算をしたかを検証可能 • DFINITY(ベンチャー) • https://dfinity.org/ • Ethereumを拡張する仮想ブロックチェーンコンピュータ(?) • ペアリングベースのBLS署名を用いて実現 • 秘密分散された公開鍵を用いて署名・集約が可能 応用例2 9 / 40
  10. 10. • 上位層はC++, 最下位層は独自アセンブリ言語 / LLVM mclのモジュール構成 準同型暗号 ペアリング 楕円曲線 有限体 Xbyak for x64 LLVM for Aarch64 WebAssembly BLS署名 10 / 40
  11. 11. • C++内DSLによるx86/x64向けJITアセンブラ • 実行時にコード生成が可能なライブラリ • https://github.com/herumi/xbyak • 暗号ライブラリを作りやすくするために開発(2007/1~) Xbyak 11 / 40
  12. 12. • Cとアセンブラ(以下asm)との連係が面倒 • 構造体が配列を含む場合 • これぐらいならちょっとしたツールで対応もできるが • template<size_t N>struct A{ int v[N];};とかだと結構辛い • asmの擬似命令を覚えるのが面倒 • %macro, %if, %imacro, etc... • アセンブラによって異なる • 構文が後づけなものが多く分かりにくい 従来のアセンブラの不満なところ(1/2) struct A { int a; int b[4]; int c; }; %define offset_A_a 0 %define offset_A_b 4 %define offset_A_c 20 なんらかの方法で asm側に伝える 12 / 40
  13. 13. • インラインアセンブラ • 64-bit Visual Studioでは廃止された • gccのインラインアセンブラは闇(注:個人の感想) • 組み込み(intrinsic)関数 • 最近はかなりよい(が) • VCはtemplateと組み合わせると辛い • asm出力を見ながら理想の状態に向けて調節するのが辛い (注:個人の感想) 従来のアセンブラの不満なところ(2/2) 13 / 40
  14. 14. • できるだけNASMに似た構文を目指す • 演算子オーバーロードでごにょごにょ • ヘッダオンリー • #includeするだけで使える • ライブラリにするとリンカの設定やコンパイラオプションに 気をつかわないといけない Xbyakの設計 mov eax, dword [ecx + eax * 4 + 8] vgatherdpd xmm1, [ebp+32+xmm2*4], xmm3 mov(eax, dword [ecx + eax * 4 + 8]); vgatherdpd(xmm1, ptr [ebp+32+xmm2*4], xmm3); NASM Xbyak 14 / 40
  15. 15. • 関数と生成される命令が完全に1対1 • CPUに応じた専用命令を使いやすい • 制御(if, else, for)はC++そのものなので書きやすい • Cのoffsetofなどのマクロで構造体などを直接扱える Xbyakの特長 // 構造体A<4>のメンバcの値の読み込み mov (eax, ptr [ecx + offsetof(A<4>, c)]); // add(eax, eax)を3回出力 for (int i = 0; i < 3; i++) { add(eax, eax); } 15 / 40
  16. 16. • 64 * n-bit加算(ビット長に応じたコード生成) 多倍長加算の例 GenAdd(int n) { for (int i = 0; i < n; i++) { mov(rax, ptr [x+i*8]); if (i == 0) add(rax, ptr [y+i*8]); else adc(rax, ptr [y+i*8]); mov(ptr [z+i*8], rax); } ret(); } add3: mov rax, [rsi] add rax, [rdx] mov [rdi], rax mov rax, [rsi + 8] adc rax, [rdx + 8] mov [rdi + 8], rax mov rax, [rsi + 16] adc rax, [rdx + 16] mov [rdi + 16], rax ret add2: mov rax, [rsi] add rax, [rdx] mov [rdi], rax mov rax, [rsi + 8] adc rax, [rdx + 8] mov[rdi + 8], eax ret N=2 N=3 16 / 40
  17. 17. • AVX-512対応依頼(2016/4) • 対応CPU存在してないのに? • マニュアル1000ページ以上 • 7月対応 • Knights Mill用のも欲しい(2016/11) • 対応したけど 確認方法が無い • 2017/12/8ローンチ >I've checked the encoding using an internal version of XED assembler/... AVX-512完全(Ice Lakeまで)対応 17 / 40
  18. 18. • Intel CPU向けに最適化されたdeep learningライブラリ • https://github.com/intel/mkl-dnn • エンジン部分がXbyakで書かれている • MKL-DNNが利用しているツール(上記urlより引用) • Caffe Optimized for Intel Architecture, DeepBench, PaddlePaddle, Tensorflow, Microsoft Cognitive Toolkit (CNTK), Apache MXNet, Intel Computer Vision SDK, etc. Intel MKL-DNN 18 / 40
  19. 19. • @chikoskiさん からの依頼 • プログラムを書くときに考えていること • 何に気を付けて設計しているのか • デバッグ • 印象に残っているバグ 後半の内容 19 / 40
  20. 20. • (私がやってる暗号関係では) • アルゴリズム • わりと高度な数学 • 論文いっぱい • 紙と鉛筆で考える • データレイアウト • いかにメモリアクセスを減らすか • ex. 仮想関数を使わない(asmからアクセスしやすい) • 基本命令のスループットとレイテンシ • 一つのことをするのに沢山の方法がある • 場合によっては選択するアルゴリズムが変わることも 開発中に考えていること 20 / 40
  21. 21. • 暗号アルゴリズムの速度は乗算(M)、加減算(A) の回数で評価することが多かった • 例. algo1はMが3回でAが5回. algo2はMが2回でAが7回 • もしMがAの10倍かかる(M=10A)なら algo1=35A algo2=27A でalgo2が速い • 2009年ごろ • SIMDを使った手法が主流 • IntelのSIMDは64bitを超えるbitシフト(S)は苦手 • が、当時それを考慮した評価はあまり無かった • Aが増えてもSが減ればトータルで高速なアルゴリズム SIMD vs. add/sub/mul(1/2) 21 / 40
  22. 22. • 2009年~ • Core i7(Nehalem)で乗算のレイテンシーが3cycleと高速化 • SIMDより64×64→128bit乗算を使う方が速いかも • SIMDを駆使した既存実装に対して2倍近い速度向上 • Haswellから利用可能なmulxなも使って世界最速 • https://github.com/herumi/ate-pairing SIMD vs. add/sub/mul(2/2) 22 / 40
  23. 23. • 呼び出し既約 • Cの関数を呼び出すときのレジスタの値の設定ルール • 例)Linuxなら第1~4引数がrdi, rsi, rdx, rcx • 関数内でr12~r15, rbx, rbpを使いたければ値を退避・復元 • プロファイル上位のほとんどの関数をasm化 • それらの関数内での呼び出し規約は自分で決めてよい • レジスタの退避・復元を最小化する呼び出し既約 • もちろんCから呼ぶとSEGV • LLVMのコード生成より1.1~1.5倍速いことも 独自呼び出し既約(禁じ手) 23 / 40 func() // 擬似コード store(rsp + 80, r12, r13, r14, r15,rbx, rbp); 計算本体 load(r12, r13, r14, r15, rbx, rbp, rsp + 80); ret(); コンパイラから 呼ばれないなら 無くしてもよい
  24. 24. • 全てC++で完結しないことが多い • 極めて速度が要求される部分だけC++ & asm • DLL化してJava, C#, Go, Pythonなどから呼び出す • C++クラスをC APIにする一般的な手順 • コンストラクタはcreateA(); • デストラクタはdestroyA(A* self); • メソッドは第一引数がA*の関数 他言語と連係 class A { void f(int x); }; struct A; A* createA(); void A_f(A* self, int x); void destroyA(A* self); C API 24 / 40
  25. 25. • JNI(Java Native interface) • Javaからネイティブコードを呼び出すための仕組み • SWIG • C/C++をPython, PHP, Java, Goなどから呼ぶためのツール • Javaと連係する場合 • C++のヘッダファイルからC APIへの変換コードを自動生成 • 自動生成されたC APIをJNIで呼び出し Java側で同等のクラスを再構築 SWIGによるJavaとの連係 25 / 40 class A { private long ptr; A() { ptr = createA(); } };
  26. 26. • 私の暗号ライブラリをSWIG経由でJavaで利用 • しかしリークするはずが無い(と思った) • 自分のC++側のコードにnew/mallocがない • std::vectorなどの標準コンテナは利用しているが • SWIGのせい? • しかしそんな基本的な部分でバグがあるか? • しかし(SWIGでないにしろ)そこにしか要因はないと思われ メモリリークの報告 26 / 40
  27. 27. • 面倒な制約 • 私はそのコードや現象を直接見られない • モニタの報告のみ • C++側での対応 • C++側で全てのメソッドに呼び出し履歴を保存 • コンストラクタは呼ばれているが デストラクタが呼ばれていないインスタンスの存在 場所の特定 27 / 40
  28. 28. • SWIGによるデストラクタの扱い • Javaのラッパークラスのfinalize()でデストラクタを呼ぶ 原因の絞り込み public class Fr { private transient long swigCPtr; protected transient boolean swigCMemOwn; protected void finalize() { delete(); } public synchronized void delete() { if (swigCPtr == 0) return; if (swigCMemOwn) { swigCMemOwn = false; Bn256JNI.delete_Fr(swigCPtr); } swigCPtr = 0; } 28 / 40
  29. 29. • 基本的にいつ呼ばれるか分からない • ループが終わってからfinalize()が呼ばれるかもしれない • プログラム終了時までに呼ばれないかもしれない • JavaはC++でどれだけメモリ確保したか知らない • 1GB x 100の「メモリプール」が発生する Javaのfinalize() for (int i = 0; i < 100; i++){ Fr *x = new Fr(); x = null; } 29 / 40
  30. 30. • 巨大なメモリを確保するインスタンスを再利用 してもらう • 「メモリリーク」が解消された 場当たり対処方法 for (int i = 0; i < 100; i++){ Fr *x = new Fr(); x.calc(...); } Fr *x = new Fr(); for (int i = 0; i < 100; i++){ x.calc(...); } 30 / 40
  31. 31. • Go • deferはインスタンスの寿命とスコープが異なるので使えない • インスタンス生成毎にruntime.SetFinalizerにdestroyAを登録 • しかしやはりいつ呼ばれるかは不明 • JavaScript(WebAssembly) • finalizerが無い • (対処方法)classがUint32Arrayを持ちメソッドごとにcopy • 細かい計算ではオーバーヘッドが大きいが、そもそも 呼び出しのオーバーヘッドが大きいので許せることもある • Rust • GCは無い • std::ops::Dropで確実にdestroyAを呼べる GCのある言語は辛い 31 / 40
  32. 32. • 必要サイズを取得するC APIを追加 • 一般解ではないがC/C++側でメモリ管理をしない戦略 • getSizeForA()が固定長のときのGoの例 メモリ管理を呼び出し側でしてもらう size_t getSizeForA(); // createA()で必要だったメモリ量 bool InitA(A* a); // 領域aにAを構築する void A_foo(A* a); type A struct { self *C.A } func NewA *A { p := new(A) p.self = C.createA() runtime.SetFinalizer(p, destroyA) return p } 32 / 40 type A sturct { v [4]C.unt64_t } func NewA *A { return new(A) }
  33. 33. • コードを書くよりバグ特定&修正の方が時間がかかる • テスト • 境界値を使ったテスト • 再現性 • 往々にして根気がいる • 場所の特定 • 最小コード • 絶対に正しいところと不安なところの境界 • 論理的思考と犯人当てゲーム • 想定外の答え • 持ち駒(有用なツール)を増やす バグ退治 33 / 40
  34. 34. • あるVM上で暗号ライブラリmclが未定義命令で落ちた • CPU自体はその命令に対応している • mclはCPUがその命令に対応してるかcpuidを用いて確認 • 対応してないと代替命令を生成 • 原因 • VM自体がまだその命令に対応していなかった • が、cpuidの誤ったエミュレーション • 対応しているという結果を返していた Illegal instruction 34 / 40
  35. 35. • gccのバージョンを上げたらある処理が4倍遅くなった • 関数のそれぞれのasm出力コードは問題なさそう • 謎の挙動 • 右のbenchが遅い • (B)と(C)の違いは? おかしな因果関係? void bench() { (A)ベンチマーク // (B) } int main() { bench(); // (C) unitTest1(); // (D) unitTest2(); // (E) unitTest3(); } 35 / 40 ここでexit()すると速い ここでexitは少し遅い ここだともう少し遅い ここでexit()すると遅い
  36. 36. • gccのinlineを管理するオプション • --param inline-unit-growth=<N>なども • gccはinline対象関数の総量を管理している • unitテスト内に大量のinline可能な関数がある • 最適化レベルが上がりinline対象関数が増える • bench内の関数がinline対象外となり遅くなる • mainの途中でexitした後のコードは生成されない • inline対象が減り、結果benchがinline化されて速くなる • bench内でexitしてもmain内でのinline対象は減らない • benchは速くならない --param max-inline-insns-single=<N> 36 / 40
  37. 37. • AVX利用時に標準数学関数が5倍遅くなる現象 • 最小コードと原因追求に苦労 Visual Studioであったバグ const struct A { float a[8]; A() { const float x = log(2.0); for (int i = 0; i < 8; i++) a[i] = x; } } notUsed; int main() { ... } 37 / 40 一度も参照されない変数 7だと遅くならない この中のsinやexpが遅くなる
  38. 38. • VCは8回ループをAVX2のシャッフル命令で最適化 • しかしVCはSSEに切り戻すコード(vzeroupper)を入れ忘れ • CPUは状態を持っている • SSE向け関数自体は問題ないので現象を特定しづらい • Intel Software Development Emulator(sde)やperfで AVX⇔SSEの切り替え回数を取得できる AVXとSSEの切り替えペナルティ 38 / 40 clean upper dirty upper AVX使用 vzeroupper SSE AVX SSE 速度低下
  39. 39. • 不具合の犯人はもちろん自分の可能性が一番高い • が、犯人が他にいないか頭の片隅に置きながら調査と推論 • コンパイラ • デバッガ • OS/device driver • ファームウェア • ハードウェア • 根気と気分転換 • cf. 「半年かかったバグ調査の顛末は」 http://blog.cybozu.io/entry/2016/01/08/080000 いろいろある 39 / 40
  40. 40. 「○○したら△だった」 「△したいなら○○すべき」 • 論理的思考 • 数学 • よいモデル • 因果関係 • 自分の得意なことを伸ばす • +αでその周辺を膨らませる • 根気 • 長く続けてるとよいことがあるかもしれない • よいツール • 先人の知恵に乗っかろう まとめ × 40 / 40

×
Save this presentationTap To Close