はじめに
こんにちは。Security R&Dチームでセキュリティ技術研究とセキュリティコンサルティングを担当しているHan Juhongです。世の中にはさまざまなセキュリティ技術があります。その中で暗号アルゴリズムは、データセキュリティに欠かせない技術です。LINEでも重要なデータを保護するため、多くの暗号アルゴリズムを適用してサービスを提供しています。私たちのチームでは、セキュリティに詳しくない社内の他のエンジニアの方も簡単で便利に使用できるよう、さまざまな暗号アルゴリズムをライブラリとして提供しています。
ライブラリAES-GCM-SIVもその1つです。最近TinkやBoringSSLなど複数のライブラリがAES-GCM-SIVを提供していますが、私たちのチームで導入した当初はこのようなライブラリがなかったため、独自に実装しなければなりませんでした。そこで、当時のチームメンバーはC言語でコードを作成し、それをJAVAやAndroid、Goのようなさまざまな環境に移植して他のチームで便利に使用できるように提供しました。また、現在LINEクライアント(AndroidおよびiOS)やLINEメッセージのスタンプの暗号化など、多くのサーバーサイドプロジェクトで使用されています。
このようにどんどん多くのチームで使用され、チームでは多くの方が利用するARM環境向けにも最適化が必要だと判断しました。今回の記事では、最近完了したARM環境向けの最適化プロセスとその結果について紹介します。
AES-GCM-SIVについて
情報セキュリティに興味がある方はAES-GCMが最も人気のある認証暗号アルゴリズムの1つであることをご存知だと思いますが、AES-GCM-SIV[1]は少し聞き慣れないかもしれません。従来のGCMモードのセキュリティをもう少し改善したアルゴリズムだと考えてください。従来のGCMアルゴリズムは、異なる2つのデータを暗号化する際、同じキーとノンス(nonce)ペアを再利用する場合に脆弱性が発生しました[2]. そのため、NISTは同じキーとノンスペアを再利用しないように勧告していますが[3]、その事実を知らないユーザーは意図せずに脆弱性を作ってしまうことがあります。AES-GCM-SIVは、この問題に耐性を持つように設計したアルゴリズムです。AES-GCM-SIVを聞きなれない方のために、その構造やメリットとデメリットについて解説します。
アルゴリズムの構造
AES-GCM-SIVは、大きく鍵導出とメッセージ認証、メッセージ暗号化の3つに分けられます。アルゴリズムの初期段階では鍵導出関数でマスターキーを入力してもらい、メッセージ認証キーと暗号化キーを生成します。その後、POLYVALとAESアルゴリズムを経て、メッセージ認証に使用するタグを生成し、この値をカウンター値として使用して暗号化するプロセスで完了します。最初に認証キーと暗号化キーを別々に生成することと認証値を先に計算してそれをカウンターとして使用して暗号文にバインドすることで以前のGCMモードよりさらに安全になりました。
メリットとデメリット
AES-GCM-SIVも他の暗号アルゴリズムと同様にメリットとデメリットがあります。
メリット
まず第1のメリットは安全性が高いことです。AES-GCM-SIVの主なコンセプトは、前述のようにメッセージを暗号化/復号する際、マスターキーを利用して暗号化キーと認証キーをそれぞれ生成することです。暗号化キーと認証キーは、AES-CTRモードで出力された乱数のように見える十分長いキーストリームで生成され、このように乱数に見える長いキーストリームは、攻撃者が予測しにくいものです。
また、攻撃者に暗号化キーや認証キーが露出されても、他のキーやマスターキーには影響がありません。ただ新しいノンスで認証キーと暗号化キーのペアを再度導出すればいいです。通常、他のアルゴリズムは、鍵導出プロセスなしにマスターキーを直接使用する場合が多くあります。もちろん、他のアルゴリズムでもプロトコルがきちんと設計されていれば、類似した構造にできると思いますが、AES-GCM-SIVは設計段階からこのような要素が組み込まれているため、セキュリティプロトコルの設計に慣れていない方でもミスを最小化できるということが大きなメリットです。
安全性に関するもう1つのメリットは、従来の認証暗号アルゴリズムで発生するノンスの再利用問題に耐性があることです。従来のGCMアルゴリズムでは、攻撃者が同じキーとノンスで作成した暗号文ペアを取得し、どちらかの平文が分かれば、残りの平文も復元することができます。このような状況では認証キーも簡単に復元できるため、攻撃者は必要な平文と暗号文、認証値のペアを必要なだけ作ることができます。一方、AES-GCM-SIVでは同じ状況で攻撃者が2つのデータを同じデータで暗号化したかどうかが分かるだけで、他に何の情報も得ることができません(もちろん、この情報も攻撃者には知らせたくないですが、これは決定論的アルゴリズムでは避けられない限界です)。
第2のメリットは、アルゴリズムを実装する際にさまざまなハードウェアリソースを利用できることです。AES-GCM-SIVは、上の図のように内部で暗号化/復号および認証キーの生成にAESアルゴリズムを使用します。AESアルゴリズムは、かなり以前から世界中で広く使われてきたため、現在はソフトウェアよりパフォーマンスが優れているハードウェアアクセラレータを搭載したCPUが多くあります。これは、実装にも大きなメリットになります。また、認証タグをを生成するためのPOLYVAL関数は標数2の体 (Binary field) 上での演算が必要ですが、最近は多くのCPUからそのための命令セット(carry-less multiplication)を別途提供しています。そのため、AES-GCM-SIVは演算量が最も多く必要なデータ認証プロセスと暗号化/復号プロセスにおいてさまざまなハードウェアリソースを使用できます。これは、ソフトウェア実装だけが可能な他のアルゴリズムに対して非常に大きなメリットになります。さらに、POLYVAL関数は、GCMモードで使用する数式と似ていますが、リトルエンディアン(little-endian)のアーキテクチャにおいてより効率的に計算できるように変形された計算式を使うため、GCM内部のGHASH実装をPOLYVALアルゴリズムの実装に再利用できるメリットもあります。
デメリット
次は、デメリットについて見てみます。すでに気づいた方もいるかもしれませんが、メリットとして話した安全な構造のため、ソフトウェアで実装すると他のアルゴリズムに対してパフォーマンスが少し落ちてしまいます。暗号化/復号するたびに、その都度内部で認証キーと暗号化キーの生成を行うため、一度生成されたラウンドキーを保存して複数回再利用する他のアルゴリズムに対してパフォーマンスが落ちてしまいます。
また他のデメリットは、アルゴリズムの構造によって暗号化/復号プロセス全体においてデータをストリーム形式で処理できないことです。これは、暗号化/復号プロセスを行う前に追加認証データ(AAD)と平文/暗号文に対しての認証プロセスを先に行う必要があるためです。ストーリム形式でデータを処理できないため、暗号化/復号プロセスを行う前にすべてのデータとメモリーリソースを使用できる状態にする必要があります。
しかし、このようなデメリットはパフォーマンスに関するデメリットなので、ハードウェアアクセラレータや特定の命令セットを使用して実装すればほとんど補うことができ、その結果ユーザーがあまり体感しない差にすることができます。
最適化プロセスについて
前述のとおり、AES-GCM-SIVアルゴリズムの実装には、AESハードウェアアクセラレーターとGCMモードのための特別な命令セットを使用できます。従来、私たちのチームから提供していたライブラリは、Intel x86-64バージョンにはMbed TLSのAESコードを使用してこのような最適化要素を追加しておきましたが、ARMアーキテクチャのバージョンにはソフトウェア環境だけを考慮した実装のみありました。しかし、他のチームで使用する頻度が少しずつ増えており、最近私たちのチームで使用する多くのサーバーがARMアーキテクチャ環境であることを考慮すると、ARM環境向けの最適化も必要だと判断しました。最適化は、暗号ハードウェアの加速機能をサポートするArmv8-A CPUにのみ適用できますが、最新のARM CPUはこの機能をサポートしない場合が多いことも考慮しました。
x86-64環境での実装とARM環境での実装は、基本的に命令セットが少し異なるだけでほぼ同じなので、この記事ではARM環境向けの実装だけを解説します。
最適化作業
通常の最適化プロセスは、ソースコードを書いた後、プロファイリングツールを利用して最適化が必要な位置を見つけ、そこを集中して最適化する流れで行います。しかし、今回は最適化する対象がAES-GCM-SIVアルゴリズム内部で使用する暗号化プロセスとデータ認証プロセスに定められていたため、プロファイリングなしに該当する部分に焦点を当てて最適化を開始しました。
最適化に関連しては通常、状況に合ったデータ型と文法、アルゴリズムでプログラミングした後、いくつかのテクニックを使うとコンパイラがより見事に最適化してくれることが多くあります。しかし、暗号アルゴリズムを最適化する場合、コンパイラによる最適化では不十分な部分があります。暗号演算プロセスには、多くの数学演算で発生するキャリーの処理からベクトル演算の最適化、CPUアーキテクチャごとのパイプライン構成、データロード(load)と保存(store)のためのレジスター制御まで、最適化する要素が多くありますが、コンパイラがこれらをすべて最適化することはできません。そのため、多くの暗号エンジニアはこの部分まで最適化するために、アセンブラを使用します。アセンブラはプログラミングが非常に難しく、開発に時間ももっとかかりますが、ハードウェアリソースを最大限効率的に使用できるという利点があります。私たちもアセンブラを使って開発しましたが、どのように最適化したかについて説明します。
AESの構造と関連命令について
前述のように、AES-GCM-SIVを最適化するためには、内部で使用するAESアルゴリズムを最適化する必要があります。ハードウェアアクセラレータを使用しましたが、説明する最適化の内容がAESの構造とも関係しているため、簡単に説明します(各プロセスの詳細については、FIPS-197[4]を参照してください)。
AESは鍵拡張部分と暗号化/復号部分に分かれています。鍵拡張部分は暗号化の各ラウンドの最終段階で使用するラウンドキーを生成する段階です。1つのラウンドキーは32-bitサイズのワード4つで生成されます。暗号化プロセスは下図のように構成されますが、最後のラウンドでMixColumns演算が抜けていることと、暗号キーのサイズが128/192/256 bitに分かれるものの生成されるラウンドキーはすべてブロックサイズに合う128-bitという点が重要です。
次は、ARMアーキテクチャで使用するAES関連の命令セットについて説明します。前述のように、AES-GCM-SIVは内部でCTRモードを使用するため、AESの復号関数を使用しません。したがって、使用する命令は2つだけになります[5][6].
- AESE - AES single round encryption
- AddRoundKey、SubBytes、ShiftRows演算がすべて以下のように処理されます。
-
AESE <Vd>.16B, <Vn>.16B { bits(128) operand1 = v[d]; bits(128) operand2 = v[n]; bits(128) result; result = operand1 EOR operand2; result = AESSubBytes(AESShiftRows(result)); v[d] = result; }
- AESMC - AES MixColumns
- 残りのMixColumns演算を処理します。
鍵拡張プロセス
鍵拡張プロセスは、全体の動作時間で多くの部分を占めていませんでした。ただ、AES-GCM-SIVでは、暗号化ごとに鍵拡張プロセスを複数回行うため、長期的に使用する観点から少しでも改善の余地があれば、最適化する価値があると判断しました。
一般的に暗号アルゴリズムの最適化には、多くの演算が必要な部分を事前に計算してテーブルにしておくルックアップテーブル方式を主に使用します。メモリーを追加で消費する代わりに、その演算を単純なテーブルの参照に置き換えることができるためです。AESソフトウェアで実装する場合も、演算量を最小化するためにこの方式を使用します。通常、AESを実装したオープンソースを見ると、S-Boxと名付けられた配列が割り当てられています。このS-Box(substitution table)が暗号化プロセスでのSubBytes変換に対応されるテーブルで、鍵拡張プロセスのSubWordと暗号化プロセスのSubBytesで使用されます。
AESの構造図で見たとおり、鍵拡張プロセスには、前のラウンドキーの最後のワードにRotWordとSubWord演算を適用するプロセスがあります。このとき、RotWordとSubWord演算は基本的に暗号化部分のShiftRows、SubBytesと同じで、1つのワードに対してのみ適用するということだけが異なります。したがって、たとえ鍵拡張プロセスだけのための特別な命令はありませんが、暗号化プロセスで使う命令を利用して以下のように最適化できます。
LDR W0, ="#RCON"
MOV W15, W12
DUP v1.4s, W15
EOR.16b v0, v0, v0
AESE v0.16b, v1.16b
UMOV W15, v0.s[0]
ROR W15, W15, #8
EOR W9, W9, W15
EOR W9, W9, W0
EOR W10, W10, W9
STP W9, W10, [%[rk]], #8
EOR W11, W11, W10
EOR W12, W12, W11
STP W11, W12, [%[rk]], #8
上記のコードで、W15には前のラウンドキーの最後のワード値が保存されている状態です。これをDUP命令を使用してv1ベクトルレジスターの各ワードの位置にコピーし、その後AESE命令の出力値を使用して適切なビット演算を経て完了します。ここで重要な実装ポイントは、AESEの前に呼び出されるEOR命令にあります。EOR命令には、入力レジスターが互いに同じであれば、同じ値をXORする意味になり、演算結果値は必ず0になります。つまり、値を0に初期化してAddRoundKeyのプロセスをなくす役割をします。EOR命令を使った変数の初期化はアセンブラプログラミングでよく使う方法なので、覚えておけば他のコードを見るとき役に立つと思います。
前述のAESE演算においてVdの値を0にして整理すると、以下のようになります。
AESE Vd, Vn
{
operand1 = Vd;
operand2 = Vn;
result;
operand2 = operand1 EOR operand2; // operand2 = 0 EOR operand2;
result = AESSubBytes(AESShiftRows(operand2));
vd = result;
}
AESE命令の先頭にoperand1とoperand2をXORする部分があるため、Vdに0が入力されない場合、以降のShiftRowsとSubBytes演算の入力値はoperand2以外の値になります。
暗号化プロセス
暗号化プロセスでも鍵拡張プロセスと同様、ルックアップテーブル方式を使って実装する場合が多いです。暗号化プロセスのテーブルにはSubBytes、ShiftRows、MixColumns演算の結果値があらかじめ計算されており、暗号化プロセスはテーブル参照と数回のビット演算の後、ラウンドキーとXORするプロセスで終わります。しかし、このときハードウェアアクセラレーション命令を使用すると、プロセッサーによって差はありますが、約6サイクルで単一ラウンド暗号化が終わり、テーブル参照をする必要がありません。実際の実装内容が気になる方のために、128-bitキー暗号化だけ書いてみました。
// Load plaintext
LD1.16b {v0}, [%[in]]
// Load roundkeys
LD1.16b {v1-v4}, [%[rk]], #64
// 1 to 4 round encryption
AESE v0.16b, v1.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v2.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v3.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v4.16b
AESMC v0.16b, v0.16b
// Load roundkeys
LD1.16b {v1-v4}, [%[rk]], #64
// 5 to 8 round encryption
AESE v0.16b, v1.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v2.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v3.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v4.16b
AESMC v0.16b, v0.16b
// Load roundkeys
LD1.16b {v1-v3}, [%[rk]]
// 9 to last round encryption
AESE v0.16b, v1.16b
AESMC v0.16b, v0.16b
AESE v0.16b, v2.16b
// Last AddRoundKey
EOR.16b v0, v0, v3
// Store ciphertext
ST1.16b {v0}, [%[out]]
コードを見ると、v0レジスターには平文を、v1~v4までは4つのラウンドキーを一度にロードし、各ラウンドの暗号化ごとに使用することが分かります(もちろん、これは一例に過ぎず、ラウンドキーのロード数と使用するベクトルレジスター数などは設計によって異なる場合があります)。ところで、上記のコードに少し違和感を感じませんか?よく考えてみると、前述した構造とよくマッチしません。AES標準ドキュメントでは、各ラウンドをSubBytes、ShiftRows、MixColumns、AddRoundKeyの順に構成しました。そして、最後のラウンドでMixColumnsのプロセスが抜けています。しかし、AESE命令の内部動作プロセスを考えると一致しません。これは、下図のように考えることで、プロセスが自然になります。最後のラウンドではAESE命令だけ呼び出し、AddRoundKeyのプロセスを経ますが、このときAddRoundKeyは、上記のコードのようにXORで簡単に処理されます。
タグ生成プロセス
次に、タグ生成プロセスの最適化について説明します。AES-GCM-SIVアルゴリズムは、GCMモードと同様にGaloisモードベースの認証メカニズムを使用します。ここではPOLYVALと呼びますが、これはただGCMモードのGHASHで使用する既約多項式の係数が逆に表現されたものです[1]。したがって、リトルエンディアン環境ではより効率的に計算することができます。下記の式は標数2の体の上で定義された式なので、POLYVAL演算をするためには標数2の体上で加算、減算、乗算、逆演算をする必要があります。
標数2の体上での表現方法や演算に慣れていない方のために簡単に説明します。まず、バイナリ多項式 (Binary polynomial) は各項の係数が0または1で構成されます。これは、コンピューターが各ビットを0または1で表現するのと同じく考えることができます。簡単に例を挙げると、 は
で表現できます。このように表現すると、次数が高くなっても配列内にすべての項を表現することができます。また、標数2の体上での演算はキャリーが伝播されないというおもしろい性質があります。そのため、加算、減算は各被演算子間のXORで処理され、乗算は一般的な乗算と似ていますが、乗算過程での加算がキャリー伝播なしにXORで処理されます。
それでは、POLYVAL関数の定義と最適化方法について本格的に説明します。POLYVAL関数の定義は以下のとおりです[1].
は、メッセージ認証キーに該当し、
はデータブロックに該当します。POLYVAL関数を経て最終的に出た結果値
は、タグ値を生成するために使います。式をよく見ると、結局
は、メッセージ認証キーとデータを使った
演算の繰り返しで生成することが分かります。そこで、この
演算部分を最適化しました。
演算は、多項式
と
の乗算と
の逆乗算、大きくこの2つの段階に分けられます。まず、
と
の乗算を図で見てみます。
POLYVAL関数に定義された標数2の体上での乗算は上の図のように表現されます。一般的な乗算と似ていますが、内部加算をXOR形式で処理し、キャリーが発生しません。図では128-bitの乗算を示していますが、私たちが使う標数2の体の乗算命令は64-bit乗算までのみ提供します。つまり、128-bitベクトルレジスター2つを入力としてもらい、全体の256-bitを出力するには、標数2の体の乗算命令を4回呼び出す必要があります。
使用する命令はPMULLとPMULL2です。PMULLは各被演算子の下位64bit同士の乗算を、PMULL2は上位64bit同士の乗算をキャリーなしで計算します[7]。128bitの乗算をコードでは以下のように表現できます。
LD1.16b {v0}, [%[msg]] // A = v0
LD1.16b {v1}, [%[key]] // B = v1
PMULL.1q v2, v0, v1 // v2 = L = AL*BL
PMULL2.1q v5, v0, v1 // v5 = H = AH*BH
EXT.16b v0, v0, v0, #8 // v0 = AH AL to AL AH
PMULL.1q v3, v0, v1 // v3 = AH*BL
PMULL2.1q v4, v0, v1 // v4 = AL*BH
EOR.16b v3, v3, v4 // v3 = M = AL*BL ^ AL*BH
MOV v4.d[0], XZR // v4 low 64-bit set zero
MOV v4.d[1], v3.d[0] // v4 = M << 64
MOV v3.d[0], v3.d[1] // v3 = M >> 64
MOV v3.d[1], XZR // v3 high 64-bit set zero
EOR.16b v2, v2, v4 // v2 = R[127:0]
EOR.16b v5, v5, v3 // v5 = R[255:128]
次に、先ほど得た2つの多項式の乗算結果に の逆元を乗算します。通常、逆元は拡張ユークリッドアルゴリズムのような方法で計算しますが、ここでは複雑な計算なしに少しの数式展開で簡単に解決できます。方法は次のとおりです。上記のv2、v5レジスターにある値は多項式が表現された値なので、v5を
に、v2を
に定義すると、先の
の結果を以下のように表現できます。
しかし、私たちが行いたい計算は 演算の定義のように、両辺に
を掛けた式でなければなりません。そこで両辺に
を掛けると、以下のように整理されます。
このときの乗算は上記のように
を2回掛けたことで表現できます。
は多項式
から以下のように計算できます。
しかし、上記の式で の計算に64-bit乗算命令を使うには、
を削除して以下のように
と表す必要があります。
後もう少しで終わりです。先にを導出した式から
を作ることができます。このとき、
を64-bitで表現すると
になり、もっと整理すると以下のような式になります。
この式を下図のように示すと、は64-bitの上位桁と下位桁を入れ替え、1回の乗算と1回のXOR演算で計算されることが分かります。
つまり、最終的に 乗算をコードで表現すると以下のとおりです。
uint64_t Inverse[2] = {0x0000000000000001, 0xc200000000000000};
LD1.16b {v6}, [%[Inverse]]
// Note1. v5 = h(x) = R[255:128]
// Note2. v2 = l(x) = R[127:0]
// Multiplication x^-64
EXT.16b v2, v2, v2, #8 // l_H l_L to l_L l_H
PMULL2.1q v3, v2, v6 // l_L x 0xc200...00
EOR.16b v2, v2, v3
// Multiplication x^-64
EXT.16b v2, v2, v2, #8 // l_H l_L to l_L l_H
PMULL2.1q v3, v2, v6 // l_L x 0xc200...00
EOR.16b v2, v2, v3
EOR.16b v0, v2, v5 // h(x) + (l(x) * x^-64 * x^-64)
ここまで適用するだけでも、かなりのパフォーマンス向上を体感できます。しかし、まだ認証タグの演算は改善の余地があります。前述のようにルックアップテーブル方式を使うことです[8]. 式を
でさらに整理すると、以下のように表現できます。
この式は深さをどのように設定するかによって拡張しつづけることができますが、上記の式でを事前に計算しておけば、
演算回数を減らすことができます。事前の計算値がない場合、
演算回数だけ
演算回数だけ
にすると、
ブロックごとに1回の
演算が必要です。
まとめますと、この方式は深さをにした場合、既存の
回の
演算が
回に減る効果があります。そのためには、
バイトのメモリーと事前の
演算が
回必要です。つまり、データサイズが大きいほど効果が大きくなりますが、もしデータサイズが小さい場合は、事前計算にかかるコストがより大きくなる可能性があるため、適用する環境のデータサイズを考慮して深さを調整する必要があります。
最適化結果の比較
上記のような方法で最適化したコードが既存コードに対し、パフォーマンスがどれだけ向上したかをグラフで簡単にまとめてみました。 グラフの数値を見ると、プロセッサーごとに少量のデータは約7倍、大量のデータは約30倍近くパフォーマンスが向上したことが分かります。
AES-GCM-SIVは、大きく認証タグ生成部分と暗号化部分に分かれます。暗号化はデータサイズに関係なく同じ数値を示しますが、認証タグ生成は先ほど述べたようにテーブルを利用した最適化を適用したため、データサイズが小さい場合は効果が微々たるもので、サイズが大きくなるにつれて効果も大きくなります。注目すべき点はデスクトップ環境だけでなく、Cortex-A55のようなモバイルプロセッサーでもパフォーマンスが大きく改善されたことです。最近はIntelだけでなく、ARMアーキテクチャベースのデバイスもAES命令とNEON命令を多くサポートしているので、暗号化が必要な環境でこのような命令をサポートする場合は、今回紹介した方法で最適化してみるのもいいと思います。
その他の改善事項とメリット
メンテナンスと可読性のため、組み込み関数に置き換え
ここまで、アセンブラを使って最適化を行なったことについて説明しました。この記事を読んで、このようにプログラミングすると可読性は非常に悪くなり、メンテナンスも難しくなると考える方もいると思います。はい、そうです。この記事には詳しく書いていませんが、実装する際には、利用可能な範囲のレジスターがいくつあるか、caller-savedレジスターとcallee-savedレジスターはどのようなものがあるか、メモリーロードと保存において非効率的なところはないかなど、考慮すべきことが多くあります。そのため、今はきちんと実装したとしても、後でメンテナンスのためにコードを修正しようとすると、多くの困難が発生するでしょう。
このような問題を防ぐために、アセンブラで開発したコードを組み込み(intrinsics)関数を使ったコードに変更しました。組み込み関数とは、各アセンブラとマッピングされ、低レベル制御を独自でできるようにするC関数です。関数のように見えますが、実際はコンパイル段階でインラインコードの形で生成されます。その結果、組み込み関数を使用すると、アセンブラを直接使用する必要がないため、プログラミングの難度が大幅に下がり、コンパイラによる支援も受けることができます。
下記の表は、AESとPOLYVAL演算で使う主な命令に対応する組み込み関数です[9]。他にもいくつかの組み込み関数をもっと使いますが、その作業はオープンソースとして公開する予定なので、気になる方は今後コードが公開されたらコードで確認してください。
2023年11月についにオープンソースAES-GCM-SIV Libraryを公開しました。AESとPOLYVAL演算で使う主な命令に対応する組み込み関数について、さらにご興味のある方は、私たちが公開したオープンソースで確認してください。
Instruction | Return Type | Name | Arguments |
---|---|---|---|
AESE | int8x16_t | vaeseq_u8 | (uint8x16_t data, uint8x16_t key) |
AESMC | int8x16_t | vaesmcq_u8 | (uint8x16_t data) |
PMULL | poly128_t | vmull_p64 | (poly64_t a, poly64_t b) |
PMULL2 | poly128_t | vmull_high_p64 | (poly64x2_t a, poly64x2_t b) |
サンプルコードのように、AES-GCM-SIVのためのコードのほとんどを組み込み関数に置き換え、置き換えた後もアセンブラだけを使用して実装と似たようなパフォーマンスを得ることができました。結局、パフォーマンスを維持しながらメンテナンスと可読性まで確保できました。
uint8x16_t aes128_encrypt(uint8x16_t key[], uint8x16_t block)
{
block = vaeseq_u8(block, key[0]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[1]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[2]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[3]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[4]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[5]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[6]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[7]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[8]);
block = vaesmcq_u8(block);
block = vaeseq_u8(block, key[9]);
block = veorq_u8(block, key[10]);
return block;
}
AES命令を利用した実装でサイドチャネルの攻撃耐性まで確保
ここまで話したように、私たちはAES命令を使用して新たにアルゴリズムを実装しました。これと関連して、前述ではパフォーマンスに焦点を当てて説明しましたが、セキュリティの観点からもう1つのメリットがあります。
最近、さまざまな暗号実装を攻撃する際には、暗号学的な分析による脆弱性攻撃以外にも、よく「サイドチャネル攻撃」と呼ばれるキャッシュ、タイミング、電力分析、エラー注入などのさまざまな攻撃が試みられています[10][11][12][13]。そのため、このような攻撃に耐性を持つことが、暗号アルゴリズムだけでなく、その実装においても重要な評価対象になります。前述でAESアルゴリズムは通常ルックアップテーブル方式を使用すると話しましたが、そのように実装すると、テーブルを参照するためのメモリーアクセス時間が毎回異なるという現象を利用したタイミング攻撃やキャッシュベースの攻撃が可能になります。そのため、最近多くのオープンソースではAESアルゴリズムが定数時間で動作するようにさまざまな方法を使用して実装しています[14]。しかし、一般的に安全性とパフォーマンスは互いに相反する場合が多く、ここでも例外ではありません。このように実装すると、複数の攻撃に対する耐性は持たせますが、パフォーマンスが大幅に低下します。結局、すべての攻撃に対して備えることは難しいので、通常は現実的な範囲内で安全性とパフォーマンスの妥協点を探して実装することになります。
しかし、今回紹介したAES命令を利用した実装方式はテーブルを参照せず、すべてのデータに対してほぼ同じ実行時間を示しています。そのため、タイミング攻撃やキャッシュベースの攻撃などの潜在的な脅威要素に対してある程度耐性を持つことができ、パフォーマンス向上だけでなく、セキュリティの面でも多くのメリットを得ることができました。
おわりに
最近多くのサービス環境で、暗号アルゴリズムを最適化するためのさまざまなハードウェアリソースを提供しています。私たちもそれを利用してAES-GCM-SIVアルゴリズムの最適化を行い、パフォーマンスをかなり改善しました。これまで、AES-GCM-SIVが他のアルゴリズムより安全ではあるものの、多くの開発者は、暗号アルゴリズムのため発生する遅延時間がサービスの可用性に影響を与えないか懸念していたかもしれません。この記事で説明したのように、今は最適化したアルゴリズムを適用することで、そのようなご心配は必要ないと思います。
コードはオープンソースとして公開する予定です。より詳しく知りたい方は、今後公開されるオープンソースを参考にすると役に立つと思います。長い文章を読んでいただきありがとうございました。
前述のように、2023年11月にAES-GCM-SIV Libraryという名前でコードをオープンソースとして公開しました。詳細についてご興味のある方には、公開されたオープンソースが参考になると思います。
参考資料
- AES-GCM-SIV: Nonce Misuse-Resistant Authenticated Encryption, https://www.rfc-editor.org/rfc/rfc8452.html
- Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS, https://www.usenix.org/system/files/conference/woot16/woot16-paper-bock.pdf
- Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC, https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
- FIPS 197: Advanced Encryption Standard (AES), https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.197.pdf
- Arm A64 Instruction Set Architecture, https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/AESE--AES-single-round-encryption-?lang=en
- Arm A64 Instruction Set Architecture, https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/AESMC--AES-mix-columns-?lang=en
- Arm A64 Instruction Set Architecture, https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/PMULL--PMULL2--Polynomial-Multiply-Long-?lang=en
- Intel® Carry-Less Multiplication Instruction and its Usage for Computing the GCM Mode, https://www.intel.cn/content/dam/www/public/us/en/documents/white-papers/carry-less-multiplication-instruction-in-gcm-mode-paper.pdf
- Arm Intrinsics, https://developer.arm.com/architectures/instruction-sets/intrinsics/
- Cache-timing attacks on AES, https://cr.yp.to/antiforgery/cachetiming-20050414.pdf
- Cache-Collision Timing Attacks Against AES, https://www.iacr.org/archive/ches2006/16/16.pdf
- A Simple Power-Analysis (SPA) Attack on Implementations of the AES Key Expansion, https://link.springer.com/chapter/10.1007/3-540-36552-4_24
- Power analysis attacks on the AES-128 S-box using Differential power analysis (DPA) and correlation power analysis (CPA), https://www.tandfonline.com/doi/full/10.1080/23742917.2016.1231523
- Faster and Timing-Attack Resistant AES-GCM, https://eprint.iacr.org/2009/129.pdf